chromium/chromeos/ash/components/phonehub/camera_roll_manager_impl.cc

// 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/camera_roll_manager_impl.h"

#include <memory>
#include <optional>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/observer_list.h"
#include "base/time/time.h"
#include "chromeos/ash/components/phonehub/camera_roll_download_manager.h"
#include "chromeos/ash/components/phonehub/camera_roll_item.h"
#include "chromeos/ash/components/phonehub/camera_roll_thumbnail_decoder_impl.h"
#include "chromeos/ash/components/phonehub/message_receiver.h"
#include "chromeos/ash/components/phonehub/message_sender.h"
#include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
#include "chromeos/ash/components/phonehub/util/histogram_util.h"
#include "chromeos/ash/services/secure_channel/public/cpp/client/connection_manager.h"
#include "chromeos/ash/services/secure_channel/public/mojom/secure_channel_types.mojom.h"

namespace ash {
namespace phonehub {

namespace {

constexpr int kMaxCameraRollItemCount = 4;

}  // namespace

CameraRollManagerImpl::CameraRollManagerImpl(
    MessageReceiver* message_receiver,
    MessageSender* message_sender,
    multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
    secure_channel::ConnectionManager* connection_manager,
    std::unique_ptr<CameraRollDownloadManager> camera_roll_download_manager)
    : message_receiver_(message_receiver),
      message_sender_(message_sender),
      multidevice_setup_client_(multidevice_setup_client),
      connection_manager_(connection_manager),
      camera_roll_download_manager_(std::move(camera_roll_download_manager)),
      thumbnail_decoder_(std::make_unique<CameraRollThumbnailDecoderImpl>()) {
  message_receiver->AddObserver(this);
  multidevice_setup_client_->AddObserver(this);
  connection_manager_->AddObserver(this);
}

CameraRollManagerImpl::~CameraRollManagerImpl() {
  message_receiver_->RemoveObserver(this);
  multidevice_setup_client_->RemoveObserver(this);
  connection_manager_->RemoveObserver(this);
}

void CameraRollManagerImpl::DownloadItem(
    const proto::CameraRollItemMetadata& item_metadata) {
  proto::FetchCameraRollItemDataRequest request;
  *request.mutable_metadata() = item_metadata;
  message_sender_->SendFetchCameraRollItemDataRequest(request);
}

void CameraRollManagerImpl::OnFetchCameraRollItemDataResponseReceived(
    const proto::FetchCameraRollItemDataResponse& response) {
  if (response.file_availability() !=
      proto::FetchCameraRollItemDataResponse::AVAILABLE) {
    util::LogCameraRollDownloadResult(
        util::CameraRollDownloadResult::kFileNotAvailable);
    NotifyCameraRollDownloadError(
        CameraRollManager::Observer::DownloadErrorType::kGenericError,
        response.metadata());
    return;
  }

  camera_roll_download_manager_->CreatePayloadFiles(
      response.payload_id(), response.metadata(),
      base::BindOnce(&CameraRollManagerImpl::OnPayloadFilesCreated,
                     weak_ptr_factory_.GetWeakPtr(), response));
}

void CameraRollManagerImpl::OnPayloadFilesCreated(
    const proto::FetchCameraRollItemDataResponse& response,
    CameraRollDownloadManager::CreatePayloadFilesResult result,
    std::optional<secure_channel::mojom::PayloadFilesPtr> payload_files) {
  switch (result) {
    case CameraRollDownloadManager::CreatePayloadFilesResult::kSuccess:
      connection_manager_->RegisterPayloadFile(
          response.payload_id(), std::move(payload_files.value()),
          base::BindRepeating(&CameraRollManagerImpl::OnFileTransferUpdate,
                              weak_ptr_factory_.GetWeakPtr(),
                              response.metadata()),
          base::BindOnce(&CameraRollManagerImpl::OnPayloadFileRegistered,
                         weak_ptr_factory_.GetWeakPtr(), response.metadata(),
                         response.payload_id()));
      break;
    case CameraRollDownloadManager::CreatePayloadFilesResult::
        kInsufficientDiskSpace:
      util::LogCameraRollDownloadResult(
          util::CameraRollDownloadResult::kInsufficientDiskSpace);
      NotifyCameraRollDownloadError(
          CameraRollManager::Observer::DownloadErrorType::kInsufficientStorage,
          response.metadata());
      break;
    case CameraRollDownloadManager::CreatePayloadFilesResult::kInvalidFileName:
      util::LogCameraRollDownloadResult(
          util::CameraRollDownloadResult::kInvalidFileName);
      NotifyCameraRollDownloadError(
          CameraRollManager::Observer::DownloadErrorType::kGenericError,
          response.metadata());
      break;
    case CameraRollDownloadManager::CreatePayloadFilesResult::
        kPayloadAlreadyExists:
      util::LogCameraRollDownloadResult(
          util::CameraRollDownloadResult::kPayloadAlreadyExists);
      NotifyCameraRollDownloadError(
          CameraRollManager::Observer::DownloadErrorType::kGenericError,
          response.metadata());
      break;
    case CameraRollDownloadManager::CreatePayloadFilesResult::
        kNotUniqueFilePath:
      util::LogCameraRollDownloadResult(
          util::CameraRollDownloadResult::kNotUniqueFilePath);
      NotifyCameraRollDownloadError(
          CameraRollManager::Observer::DownloadErrorType::kGenericError,
          response.metadata());
      break;
  }
}

void CameraRollManagerImpl::OnPayloadFileRegistered(
    const proto::CameraRollItemMetadata& metadata,
    int64_t payload_id,
    bool success) {
  if (!success) {
    camera_roll_download_manager_->DeleteFile(payload_id);
    util::LogCameraRollDownloadResult(
        util::CameraRollDownloadResult::kTargetFileNotAccessible);
    NotifyCameraRollDownloadError(
        CameraRollManager::Observer::DownloadErrorType::kGenericError,
        metadata);
    return;
  }

  proto::InitiateCameraRollItemTransferRequest request;
  *request.mutable_metadata() = metadata;
  request.set_payload_id(payload_id);
  message_sender_->SendInitiateCameraRollItemTransferRequest(request);
}

void CameraRollManagerImpl::OnFileTransferUpdate(
    const proto::CameraRollItemMetadata& metadata,
    secure_channel::mojom::FileTransferUpdatePtr update) {
  switch (update->status) {
    case secure_channel::mojom::FileTransferStatus::kInProgress:
      break;
    case secure_channel::mojom::FileTransferStatus::kSuccess:
      util::LogCameraRollDownloadResult(
          util::CameraRollDownloadResult::kSuccess);
      break;
    case secure_channel::mojom::FileTransferStatus::kFailure:
      util::LogCameraRollDownloadResult(
          util::CameraRollDownloadResult::kTransferFailed);
      NotifyCameraRollDownloadError(
          CameraRollManager::Observer::DownloadErrorType::kNetworkConnection,
          metadata);
      break;
    case secure_channel::mojom::FileTransferStatus::kCanceled:
      util::LogCameraRollDownloadResult(
          util::CameraRollDownloadResult::kTransferCanceled);
      NotifyCameraRollDownloadError(
          CameraRollManager::Observer::DownloadErrorType::kNetworkConnection,
          metadata);
      break;
  }

  camera_roll_download_manager_->UpdateDownloadProgress(std::move(update));
}

void CameraRollManagerImpl::OnPhoneStatusSnapshotReceived(
    proto::PhoneStatusSnapshot phone_status_snapshot) {
  UpdateCameraRollAccessStateAndNotifyIfNeeded(
      phone_status_snapshot.properties().camera_roll_access_state());
  if (!is_android_storage_granted_ || !IsCameraRollSettingEnabled()) {
    ClearCurrentItems();
    CancelPendingThumbnailRequests();
    return;
  }

  SendFetchCameraRollItemsRequest();
}

void CameraRollManagerImpl::OnPhoneStatusUpdateReceived(
    proto::PhoneStatusUpdate phone_status_update) {
  UpdateCameraRollAccessStateAndNotifyIfNeeded(
      phone_status_update.properties().camera_roll_access_state());
  if (!is_android_storage_granted_ || !IsCameraRollSettingEnabled()) {
    ClearCurrentItems();
    CancelPendingThumbnailRequests();
    return;
  }

  if (phone_status_update.has_camera_roll_updates()) {
    SendFetchCameraRollItemsRequest();
  }
}

void CameraRollManagerImpl::OnFetchCameraRollItemsResponseReceived(
    const proto::FetchCameraRollItemsResponse& response) {
  thumbnail_decoder_->BatchDecode(
      response, current_items(),
      base::BindOnce(&CameraRollManagerImpl::OnItemThumbnailsDecoded,
                     thumbnail_decoder_weak_ptr_factory_.GetWeakPtr()));
}

void CameraRollManagerImpl::SendFetchCameraRollItemsRequest() {
  // Clears pending thumbnail decode requests to avoid changing the current item
  // set after sending it with the |FetchCameraRollItemsRequest|. These pending
  // thumbnails will be invalidated anyway when the new response is received.
  CancelPendingThumbnailRequests();

  // Do not update the timestamp if it is already set. It means that there's an
  // in-progress request. We want to measure the time it takes from the first
  // time we request an update to when the UI is updated. This is the time the
  // user spends waiting.
  if (!fetch_items_request_start_timestamp_) {
    fetch_items_request_start_timestamp_ = base::TimeTicks::Now();
  }

  proto::FetchCameraRollItemsRequest request;
  request.set_max_item_count(kMaxCameraRollItemCount);
  for (const CameraRollItem& current_item : current_items()) {
    *request.add_current_item_metadata() = current_item.metadata();
  }
  message_sender_->SendFetchCameraRollItemsRequest(request);
}

void CameraRollManagerImpl::OnItemThumbnailsDecoded(
    CameraRollThumbnailDecoder::BatchDecodeResult result,
    const std::vector<CameraRollItem>& items) {
  if (result == CameraRollThumbnailDecoder::BatchDecodeResult::kCompleted) {
    if (fetch_items_request_start_timestamp_) {
      base::UmaHistogramMediumTimes(
          "PhoneHub.CameraRoll.Latency.RefreshItems",
          base::TimeTicks::Now() - *fetch_items_request_start_timestamp_);
      fetch_items_request_start_timestamp_.reset();
    }
    SetCurrentItems(items);
  }
}

void CameraRollManagerImpl::CancelPendingThumbnailRequests() {
  thumbnail_decoder_weak_ptr_factory_.InvalidateWeakPtrs();
}

bool CameraRollManagerImpl::IsCameraRollSettingEnabled() {
  multidevice_setup::mojom::FeatureState camera_roll_feature_state =
      multidevice_setup_client_->GetFeatureState(
          multidevice_setup::mojom::Feature::kPhoneHubCameraRoll);
  return camera_roll_feature_state ==
         multidevice_setup::mojom::FeatureState::kEnabledByUser;
}

void CameraRollManagerImpl::OnFeatureStatesChanged(
    const multidevice_setup::MultiDeviceSetupClient::FeatureStatesMap&
        feature_states_map) {
  if (!IsCameraRollSettingEnabled()) {
    // ClearCurrentItems() would also call ComputeAndUpdateUiState()
    ClearCurrentItems();
    CancelPendingThumbnailRequests();
  } else {
    ComputeAndUpdateUiState();
  }
}

void CameraRollManagerImpl::OnConnectionStatusChanged() {
  if (connection_manager_->GetStatus() ==
      secure_channel::ConnectionManager::Status::kDisconnected) {
    ClearCurrentItems();
    CancelPendingThumbnailRequests();
  }
}

void CameraRollManagerImpl::UpdateCameraRollAccessStateAndNotifyIfNeeded(
    const proto::CameraRollAccessState& access_state) {
  bool updated_storage_granted = access_state.storage_permission_granted();
  if (is_android_storage_granted_ != updated_storage_granted) {
    is_android_storage_granted_ = updated_storage_granted;

    util::LogCameraRollAndroidHasStorageAccessPermission(
        is_android_storage_granted_);
    ComputeAndUpdateUiState();
  }
}

void CameraRollManagerImpl::ComputeAndUpdateUiState() {
  if (!is_android_storage_granted_) {
    ui_state_ = CameraRollUiState::NO_STORAGE_PERMISSION;
    NotifyCameraRollViewUiStateUpdated();
    return;
  }

  multidevice_setup::mojom::FeatureState feature_state =
      multidevice_setup_client_->GetFeatureState(
          multidevice_setup::mojom::Feature::kPhoneHubCameraRoll);
  switch (feature_state) {
    case multidevice_setup::mojom::FeatureState::kDisabledByUser:
      ui_state_ = CameraRollUiState::SHOULD_HIDE;
      ;
      break;
    case multidevice_setup::mojom::FeatureState::kEnabledByUser:
      if (current_items().empty()) {
        ui_state_ = CameraRollUiState::SHOULD_HIDE;
      } else {
        ui_state_ = CameraRollUiState::ITEMS_VISIBLE;
      }
      break;
    default:
      ui_state_ = CameraRollUiState::SHOULD_HIDE;
      break;
  }
  NotifyCameraRollViewUiStateUpdated();
}

}  // namespace phonehub
}  // namespace ash