chromium/ios/chrome/browser/download/model/document_download_tab_helper.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/download/model/document_download_tab_helper.h"

#import "base/files/file_path.h"
#import "base/functional/callback.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/string_number_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "ios/chrome/browser/download/model/document_download_tab_helper_metrics.h"
#import "ios/chrome/browser/download/model/download_manager_tab_helper.h"
#import "ios/chrome/browser/download/model/download_mimetype_util.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/web/public/download/download_controller.h"
#import "ios/web/public/download/download_task.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "net/http/http_response_headers.h"

namespace {
// Whether `state` indicates a final state.
bool TaskStateIsDone(web::DownloadTask::State state) {
  switch (state) {
    case web::DownloadTask::State::kNotStarted:
    case web::DownloadTask::State::kInProgress:
      return false;
    case web::DownloadTask::State::kCancelled:
    case web::DownloadTask::State::kComplete:
    case web::DownloadTask::State::kFailed:
    case web::DownloadTask::State::kFailedNotResumable:
      return true;
  }
}

// Transitions `old_state` based on a new observed state.
// In some cases, it is possible that `Cancel()` will be called on a task which
// is already "done" i.e. Cancelled, Complete, Failed or FailedNotResumable. In
// that case the relevant state for metrics is the state before `Cancel()` is
// called. The `Cancelled` bucket only makes sense if the task went from
// `NotStarted`/`InProgress` to `Cancelled`.
web::DownloadTask::State NewObservedTaskState(
    web::DownloadTask::State old_state,
    web::DownloadTask::State new_state) {
  if (new_state != web::DownloadTask::State::kCancelled) {
    return new_state;
  }
  if (TaskStateIsDone(old_state)) {
    return old_state;
  }
  return new_state;
}

}  // namespace

DocumentDownloadTabHelper::DocumentDownloadTabHelper(web::WebState* web_state)
    : web_state_(web_state) {
  DCHECK(web_state);
  web_state->AddObserver(this);
}

DocumentDownloadTabHelper::~DocumentDownloadTabHelper() {
  if (observed_task_) {
    if (current_task_is_document_download_) {
      base::UmaHistogramEnumeration(
          kIOSDocumentDownloadFinalState,
          TaskStateToWebStateContentDownloadState(observed_task_state_));
      if (task_uuid_) {
        base::UmaHistogramEnumeration(
            kIOSDocumentDownloadStateAtNavigation,
            TaskStateToWebStateContentDownloadState(observed_task_state_));
      }
    }
    observed_task_->RemoveObserver(this);
    observed_task_ = nullptr;
  }
  if (web_state_) {
    web_state_->RemoveObserver(this);
    web_state_ = nullptr;
  }
}

bool DocumentDownloadTabHelper::IsDownloadTaskCreatedByCurrentTabHelper() {
  return current_task_is_document_download_;
}

#pragma mark - web::WebStateObserver

void DocumentDownloadTabHelper::WebStateDestroyed(web::WebState* web_state) {
  DetachFullscreen();
  if (observed_task_) {
    if (current_task_is_document_download_) {
      base::UmaHistogramEnumeration(
          kIOSDocumentDownloadFinalState,
          TaskStateToWebStateContentDownloadState(observed_task_state_));
      if (task_uuid_) {
        base::UmaHistogramEnumeration(
            kIOSDocumentDownloadStateAtNavigation,
            TaskStateToWebStateContentDownloadState(observed_task_state_));
      }
    }
    observed_task_->RemoveObserver(this);
    observed_task_ = nullptr;
  }

  web_state_->RemoveObserver(this);
  web_state_ = nullptr;
}

void DocumentDownloadTabHelper::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  DetachFullscreen();
  if (waiting_for_previous_task_) {
    base::UmaHistogramEnumeration(
        kIOSDocumentDownloadConflictResolution,
        DocumentDownloadConflictResolution::kPreviousDownloadDidNotFinish);
  }
  waiting_for_previous_task_ = false;
  if (task_uuid_) {
    // If a task was created on the previous navigation but was never started
    // by the user, cancel it.
    DownloadManagerTabHelper* tab_helper =
        DownloadManagerTabHelper::FromWebState(web_state_);
    CHECK(tab_helper);
    web::DownloadTask* active_task = tab_helper->GetActiveDownloadTask();
    if (active_task &&
        [active_task->GetIdentifier() isEqualToString:task_uuid_]) {
      base::UmaHistogramEnumeration(
          kIOSDocumentDownloadStateAtNavigation,
          TaskStateToWebStateContentDownloadState(observed_task_state_));
      if (active_task->GetState() == web::DownloadTask::State::kNotStarted) {
        base::UmaHistogramEnumeration(
            kIOSDocumentDownloadFinalState,
            TaskStateToWebStateContentDownloadState(
                web::DownloadTask::State::kNotStarted));
        if (observed_task_) {
          observed_task_->RemoveObserver(this);
          current_task_is_document_download_ = false;
          observed_task_ = nullptr;
        }
        active_task->Cancel();
      }
    }
    task_uuid_ = nil;
  }
}

void DocumentDownloadTabHelper::DidFinishNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  file_size_ = -1;
  if (navigation_context->GetError() != nil) {
    return;
  }
  net::HttpResponseHeaders* headers = navigation_context->GetResponseHeaders();
  if (!headers) {
    return;
  }
  std::string content_size;
  if (!headers->GetNormalizedHeader("Content-Length", &content_size)) {
    return;
  }
  int64_t file_size;
  if (!base::StringToInt64(content_size, &file_size)) {
    return;
  }
  file_size_ = file_size <= 0 ? -1 : file_size;
}

void DocumentDownloadTabHelper::PageLoaded(
    web::WebState* web_state,
    web::PageLoadCompletionStatus load_completion_status) {
  DownloadManagerTabHelper* tab_helper =
      DownloadManagerTabHelper::FromWebState(web_state_);
  // Only trigger on success.
  bool should_trigger =
      load_completion_status == web::PageLoadCompletionStatus::SUCCESS;

  // Only trigger for non HTML document.
  should_trigger =
      should_trigger &&
      (!web_state->ContentIsHTML() &&
       !base::StartsWith(web_state->GetContentsMimeType(), "video/"));

  // Only triggers on http(s).
  GURL url = web_state->GetLastCommittedURL();
  should_trigger = should_trigger && url.SchemeIsHTTPOrHTTPS();

  if (should_trigger) {
    base::UmaHistogramEnumeration(kIOSDocumentDownloadMimeType,
                                  GetDownloadMimeTypeResultFromMimeType(
                                      web_state->GetContentsMimeType()));
    // -1 will be reported in the underflow bucket, the same as the 0 bucket.
    base::UmaHistogramMemoryMB(
        kIOSDocumentDownloadSizeInMB,
        file_size_ == -1 ? file_size_ : file_size_ / 1024 / 1024);

    web::DownloadTask* active_task = tab_helper->GetActiveDownloadTask();
    if (active_task) {
      if (observed_task_) {
        observed_task_->RemoveObserver(this);
      }
      // There is already an active download task. We don't want to prompt the
      // user on page load, so just observe this task. If it ever finishes while
      // the document is still displayed, we will trigger the new download.
      waiting_for_previous_task_ = true;
      active_task->AddObserver(this);
      current_task_is_document_download_ = false;
      observed_task_ = active_task;
      observed_task_state_ = observed_task_->GetState();
    } else {
      // There is no download running at the moment.
      // Create a new one to download the document on the current page.
      task_uuid_ = [NSUUID UUID].UUIDString;
      web::DownloadController::FromBrowserState(web_state_->GetBrowserState())
          ->CreateWebStateDownloadTask(web_state_, task_uuid_, file_size_);
      web::DownloadTask* new_task = tab_helper->GetActiveDownloadTask();
      if (new_task) {
        new_task->AddObserver(this);
        observed_task_ = new_task;
        observed_task_state_ = observed_task_->GetState();
        current_task_is_document_download_ = true;
      }
      AttachFullscreen();
      base::UmaHistogramEnumeration(
          kIOSDocumentDownloadConflictResolution,
          DocumentDownloadConflictResolution::kNoConflict);
      return;
    }
  }
  DetachFullscreen();
}

void DocumentDownloadTabHelper::WasShown(web::WebState* web_state) {
  AttachFullscreen();
}

void DocumentDownloadTabHelper::WasHidden(web::WebState* web_state) {
  DetachFullscreen();
}

#pragma mark - web::DownloadTaskObserver

void DocumentDownloadTabHelper::OnDownloadUpdated(web::DownloadTask* task) {
  observed_task_state_ =
      NewObservedTaskState(observed_task_state_, task->GetState());

  if (waiting_for_previous_task_) {
    return;
  }
  if (!task_uuid_) {
    task->RemoveObserver(this);
    observed_task_ = nullptr;
    DetachFullscreen();
    return;
  }
  if (![task->GetIdentifier() isEqualToString:task_uuid_]) {
    return;
  }
  DetachFullscreen();
}

void DocumentDownloadTabHelper::OnDownloadDestroyed(web::DownloadTask* task) {
  // Depending on the order in which observers are called,
  // OnDownloadUpdated(kCancelled) may or may not have been called.
  // To uniformize the reporting, update the state here.
  observed_task_state_ = NewObservedTaskState(
      observed_task_state_, web::DownloadTask::State::kCancelled);

  task->RemoveObserver(this);
  observed_task_ = nullptr;

  if (current_task_is_document_download_) {
    base::UmaHistogramEnumeration(
        kIOSDocumentDownloadFinalState,
        TaskStateToWebStateContentDownloadState(observed_task_state_));
    if (task_uuid_) {
      base::UmaHistogramEnumeration(
          kIOSDocumentDownloadStateAtNavigation,
          TaskStateToWebStateContentDownloadState(observed_task_state_));
    }
  }

  if (!waiting_for_previous_task_) {
    DetachFullscreen();
    return;
  }

  base::UmaHistogramEnumeration(
      kIOSDocumentDownloadConflictResolution,
      observed_task_state_ == web::DownloadTask::State::kCancelled
          ? DocumentDownloadConflictResolution::kPreviousDownloadWasCancelled
          : DocumentDownloadConflictResolution::kPreviousDownloadCompleted);

  // Post this task to let the other observer trigger (in particular,
  // DownloadManagerTabHelper will set a new active task).
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&DocumentDownloadTabHelper::OnPreviousTaskDeleted,
                     weak_ptr_factory_.GetWeakPtr()));
}

#pragma mark - Private

void DocumentDownloadTabHelper::AttachFullscreen() {
  if (!task_uuid_) {
    return;
  }
  DownloadManagerTabHelper* tab_helper =
      DownloadManagerTabHelper::FromWebState(web_state_);
  web::DownloadTask* active_task = tab_helper->GetActiveDownloadTask();
  if (!active_task) {
    return;
  }
  if (![active_task->GetIdentifier() isEqualToString:task_uuid_] ||
      active_task->GetState() != web::DownloadTask::State::kNotStarted) {
    return;
  }
  tab_helper->AdaptToFullscreen(true);
}

void DocumentDownloadTabHelper::DetachFullscreen() {
  DownloadManagerTabHelper* tab_helper =
      DownloadManagerTabHelper::FromWebState(web_state_);
  tab_helper->AdaptToFullscreen(false);
}

void DocumentDownloadTabHelper::OnPreviousTaskDeleted() {
  DownloadManagerTabHelper* tab_helper =
      DownloadManagerTabHelper::FromWebState(web_state_);
  web::DownloadTask* active_task = tab_helper->GetActiveDownloadTask();
  if (active_task) {
    // There is still an active task. Still wait.
    active_task->AddObserver(this);
    current_task_is_document_download_ = false;
    observed_task_ = active_task;
    observed_task_state_ = observed_task_->GetState();
    waiting_for_previous_task_ = true;
    return;
  }

  // It is our turn. Trigger the download.
  waiting_for_previous_task_ = false;
  task_uuid_ = [NSUUID UUID].UUIDString;
  web::DownloadController::FromBrowserState(web_state_->GetBrowserState())
      ->CreateWebStateDownloadTask(web_state_, task_uuid_, file_size_);

  web::DownloadTask* new_task = tab_helper->GetActiveDownloadTask();
  if (new_task) {
    new_task->AddObserver(this);
    observed_task_ = new_task;
    observed_task_state_ = observed_task_->GetState();
    current_task_is_document_download_ = true;
  }
  AttachFullscreen();
}

WEB_STATE_USER_DATA_KEY_IMPL(DocumentDownloadTabHelper)