chromium/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_notification_manager.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 "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_notification_manager.h"

#include "ash/public/cpp/notification_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/message_formatter.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h"
#include "chrome/grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/strings/grit/ui_chromeos_strings.h"
#include "ui/message_center/public/cpp/notification_delegate.h"

namespace ash::cloud_upload {
namespace {

// The minimum amount of time for which the "in progress" state should be
// displayed.
const base::TimeDelta kMinInProgressTime = base::Seconds(5);

// Time for which the "Complete" notification should display.
const base::TimeDelta kCompleteNotificationTime = base::Seconds(5);

// If no other class instance holds a reference to the notification manager, the
// notification manager goes out of scope.
void OnNotificationManagerDone(
    scoped_refptr<CloudUploadNotificationManager> notification_manager) {}

}  // namespace

CloudUploadNotificationManager::CloudUploadNotificationManager(
    Profile* profile,
    const std::string& cloud_provider_name,
    const std::string& target_app_name,
    int num_files,
    UploadType upload_type)
    : profile_(profile),
      cloud_provider_name_(cloud_provider_name),
      target_app_name_(target_app_name),
      num_files_(num_files),
      upload_type_(upload_type) {
  // Generate a unique ID for the cloud upload notifications.
  notification_id_ =
      "cloud-upload-" +
      base::NumberToString(
          ++CloudUploadNotificationManager::notification_manager_counter_);

  // Set the system notification source display name to "Files".
  display_source_ =
      l10n_util::GetStringUTF16(IDS_ASH_MESSAGE_CENTER_SYSTEM_APP_NAME_FILES);

  // Keep the new `CloudUploadNotificationManager` instance alive at least until
  // `OnNotificationManagerDone` executes.
  callback_ =
      base::BindOnce(&OnNotificationManagerDone, base::WrapRefCounted(this));
}

CloudUploadNotificationManager::~CloudUploadNotificationManager() {
  // Make sure open notifications are dismissed before the notification manager
  // goes out of scope.
  CloseNotification();
}

NotificationDisplayService*
CloudUploadNotificationManager::GetNotificationDisplayService() {
  return NotificationDisplayServiceFactory::GetForProfile(profile_);
}

std::unique_ptr<message_center::Notification>
CloudUploadNotificationManager::CreateUploadProgressNotification() {
  bool is_copy_operation = upload_type_ == UploadType::kCopy;
  // TODO(b/242685536) Use "files" for multi-files when support for
  // multi-files is added.
  std::u16string title = base::i18n::MessageFormatter::FormatWithNumberedArgs(
      l10n_util::GetStringUTF16(is_copy_operation
                                    ? IDS_OFFICE_NOTIFICATION_COPYING_FILES
                                    : IDS_OFFICE_NOTIFICATION_MOVING_FILES),
      num_files_, cloud_provider_name_);
  std::u16string message = base::i18n::MessageFormatter::FormatWithNumberedArgs(
      l10n_util::GetStringUTF16(IDS_OFFICE_NOTIFICATION_FILES_WILL_OPEN),
      num_files_, target_app_name_);

  auto notification = ash::CreateSystemNotificationPtr(
      /*type=*/message_center::NOTIFICATION_TYPE_PROGRESS,
      /*id=*/notification_id_, title,
      // TODO(b/272601262) Display or delete this message.
      /*message=*/{}, /*display_source=*/display_source_,
      /*origin_url=*/GURL(), /*notifier_id=*/message_center::NotifierId(),
      /*optional_fields=*/{},
      /*delegate=*/
      base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
          base::BindRepeating(
              &CloudUploadNotificationManager::HandleProgressNotificationClick,
              weak_ptr_factory_.GetWeakPtr())),
      /*small_image=*/ash::kFolderIcon,
      /*warning_level=*/message_center::SystemNotificationWarningLevel::NORMAL);

  // For a progress notification the message parameter won't be displayed in the
  // notification. Therefore, its value is passed to progress_status which will
  // be displayed.
  notification->set_progress_status(message);

  // Add "Cancel" button if upload still cancellable.
  if (CanCancel() && cancel_callback_) {
    std::vector<message_center::ButtonInfo> notification_buttons = {
        message_center::ButtonInfo(
            l10n_util::GetStringUTF16(IDS_FILE_BROWSER_CANCEL_LABEL))};
    notification->set_buttons(notification_buttons);
  }

  return notification;
}

std::unique_ptr<message_center::Notification>
CloudUploadNotificationManager::CreateUploadCompleteNotification() {
  bool is_copy_operation = upload_type_ == UploadType::kCopy;
  // TODO(b/242685536) Use "files" for multi-files when support for multi-files
  // is added.
  std::u16string title = base::i18n::MessageFormatter::FormatWithNumberedArgs(
      l10n_util::GetStringUTF16(is_copy_operation
                                    ? IDS_OFFICE_NOTIFICATION_FILES_COPIED
                                    : IDS_OFFICE_NOTIFICATION_FILES_MOVED),
      num_files_, cloud_provider_name_);
  std::u16string message =
      l10n_util::GetStringFUTF16(IDS_OFFICE_NOTIFICATION_FILES_OPENING,
                                 base::UTF8ToUTF16(target_app_name_));
  auto notification = ash::CreateSystemNotificationPtr(
      /*type=*/message_center::NOTIFICATION_TYPE_SIMPLE,
      /*id=*/notification_id_, title, message,
      /*display_source=*/display_source_,
      /*origin_url=*/GURL(), /*notifier_id=*/message_center::NotifierId(),
      /*optional_fields=*/{},
      /*delegate=*/
      base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
          base::BindRepeating(
              &CloudUploadNotificationManager::HandleCompleteNotificationClick,
              weak_ptr_factory_.GetWeakPtr())),
      /*small_image=*/ash::kFolderIcon,
      /*warning_level=*/
      message_center::SystemNotificationWarningLevel::NORMAL);

  DCHECK(!destination_path_.empty());
  if (!destination_path_.empty()) {
    //  Add "Show in folder" button.
    std::u16string button_title = l10n_util::GetStringUTF16(
        IDS_OFFICE_NOTIFICATION_SHOW_IN_FOLDER_BUTTON);
    std::vector<message_center::ButtonInfo> notification_buttons = {
        message_center::ButtonInfo(button_title)};
    notification->set_buttons(notification_buttons);
  }
  return notification;
}

std::unique_ptr<message_center::Notification>
CloudUploadNotificationManager::CreateUploadErrorNotification(
    std::string message) {
  bool is_copy_operation = upload_type_ == UploadType::kCopy;
  std::u16string title = base::i18n::MessageFormatter::FormatWithNumberedArgs(
      l10n_util::GetStringUTF16(is_copy_operation
                                    ? IDS_OFFICE_UPLOAD_ERROR_CANT_COPY_FILES
                                    : IDS_OFFICE_UPLOAD_ERROR_CANT_MOVE_FILES),
      num_files_, cloud_provider_name_);
  std::vector<message_center::ButtonInfo> notification_buttons;

  auto notification = ash::CreateSystemNotificationPtr(
      /*type=*/message_center::NOTIFICATION_TYPE_SIMPLE,
      /*id=*/notification_id_, title, base::UTF8ToUTF16(message),
      /*display_source=*/display_source_,
      /*origin_url=*/GURL(), /*notifier_id=*/message_center::NotifierId(),
      /*optional_fields=*/{},
      /*delegate=*/
      base::MakeRefCounted<message_center::HandleNotificationClickDelegate>(
          base::BindRepeating(
              &CloudUploadNotificationManager::HandleErrorNotificationClick,
              weak_ptr_factory_.GetWeakPtr())),
      /*small_image=*/ash::kFolderIcon,
      /*warning_level=*/
      message_center::SystemNotificationWarningLevel::WARNING);

  //  Add "Sign in" button if this is a reauthentication error.
  if (message == GetReauthenticationRequiredMessage()) {
    std::u16string button_title =
        l10n_util::GetStringUTF16(IDS_OFFICE_NOTIFICATION_SIGN_IN_BUTTON);
    notification_buttons.emplace_back(button_title);
  }

  notification->set_buttons(notification_buttons);
  return notification;
}

void CloudUploadNotificationManager::ShowUploadProgress(int progress) {
  progress_ = progress;
  std::unique_ptr<message_center::Notification> notification =
      CreateUploadProgressNotification();
  notification->set_progress(progress_);
  // Set never_timeout with the highest priority, SYSTEM_PRIORITY, so that the
  // notification never times out.
  notification->set_never_timeout(true);
  notification->SetSystemPriority();
  GetNotificationDisplayService()->Display(NotificationHandler::Type::TRANSIENT,
                                           *notification,
                                           /*metadata=*/nullptr);

  // Make sure we display the "in progress" state for a minimum amount of time.
  if (state_ == State::kUninitialized) {
    state_ = State::kInProgress;
    in_progress_timer_.Start(
        FROM_HERE, kMinInProgressTime,
        base::BindOnce(
            &CloudUploadNotificationManager::OnMinInProgressTimeReached,
            weak_ptr_factory_.GetWeakPtr()));
  }
}

void CloudUploadNotificationManager::ShowCompleteNotification() {
  DCHECK_EQ(state_, State::kComplete);
  std::unique_ptr<message_center::Notification> notification =
      CreateUploadCompleteNotification();
  // Set never_timeout with the highest priority, SYSTEM_PRIORITY, so that the
  // notification never times out.
  notification->set_never_timeout(true);
  notification->SetSystemPriority();
  // Close the progress notification before displaying the completed
  // notification.
  GetNotificationDisplayService()->Close(NotificationHandler::Type::TRANSIENT,
                                         notification_id_);
  GetNotificationDisplayService()->Display(NotificationHandler::Type::TRANSIENT,
                                           *notification,
                                           /*metadata=*/nullptr);

  // Start the timer to automatically dismiss the "Complete" notification if
  // "Show in folder" button not clicked.
  complete_notification_timer_.Start(
      FROM_HERE, kCompleteNotificationTime,
      base::BindOnce(&CloudUploadNotificationManager::CloseNotification,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CloudUploadNotificationManager::MarkUploadComplete() {
  // Check if the "in progress" timeout has happened yet or not.
  if (state_ == State::kInProgress) {
    state_ = State::kWaitingForInProgressTimeout;
    // Re-show progress notification without the "Cancel" button as the upload
    // has now finished.
    ShowUploadProgress(progress_);
  } else if (state_ == State::kUninitialized ||
             state_ == State::kInProgressTimedOut) {
    // If the complete notification is shown before any progress notifications,
    // we don't run the kMinInProgressTime timeout.
    state_ = State::kComplete;
    ShowCompleteNotification();
  }
}

void CloudUploadNotificationManager::ShowUploadError(
    const std::string& message) {
  std::unique_ptr<message_center::Notification> notification =
      CreateUploadErrorNotification(message);
  // Set never_timeout with the highest priority, SYSTEM_PRIORITY, so that the
  // notification never times out.
  notification->set_never_timeout(true);
  notification->SetSystemPriority();
  GetNotificationDisplayService()->Display(NotificationHandler::Type::TRANSIENT,
                                           *notification,
                                           /*metadata=*/nullptr);
}

void CloudUploadNotificationManager::CloseNotification() {
  GetNotificationDisplayService()->Close(NotificationHandler::Type::TRANSIENT,
                                         notification_id_);
  in_progress_timer_.Stop();
  complete_notification_timer_.Stop();
  if (callback_) {
    std::move(callback_).Run();
  }
}

void CloudUploadNotificationManager::OnMinInProgressTimeReached() {
  if (state_ == State::kInProgress) {
    state_ = State::kInProgressTimedOut;
  } else if (state_ == State::kWaitingForInProgressTimeout) {
    state_ = State::kComplete;
    ShowCompleteNotification();
  }
}

void CloudUploadNotificationManager::HandleProgressNotificationClick(
    std::optional<int> button_index) {
  // If the "Cancel" button was pressed, rather than a click to somewhere
  // else in the notification.
  if (button_index && cancel_callback_ && CanCancel()) {
    // Cancel upload.
    std::move(cancel_callback_).Run();
  }

  CloseNotification();
}

void CloudUploadNotificationManager::HandleErrorNotificationClick(
    std::optional<int> button_index) {
  // If the "Sign in" button was pressed, rather than a click to somewhere
  // else in the notification.
  if (button_index) {
    // Request an ODFS mount which will trigger reauthentication.
    RequestODFSMount(profile_, base::DoNothing());
  }

  CloseNotification();
}

void CloudUploadNotificationManager::HandleCompleteNotificationClick(
    std::optional<int> button_index) {
  if (callback_for_testing_) {
    std::move(callback_for_testing_).Run(destination_path_);
  } else if (button_index) {
    // Show In Folder if button was pressed, rather than a click to somewhere
    // else in the notification.
    platform_util::ShowItemInFolder(profile_, destination_path_);
  }

  CloseNotification();
}

bool CloudUploadNotificationManager::CanCancel() {
  return state_ == State::kUninitialized || state_ == State::kInProgress ||
         state_ == State::kInProgressTimedOut;
}

}  // namespace ash::cloud_upload