chromium/ios/chrome/browser/snapshots/model/snapshot_tab_helper.mm

// Copyright 2017 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/snapshots/model/snapshot_tab_helper.h"

#import "base/functional/bind.h"
#import "base/memory/ptr_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/task/sequenced_task_runner.h"
#import "ios/chrome/browser/snapshots/model/features.h"
#import "ios/chrome/browser/snapshots/model/legacy_snapshot_generator.h"
#import "ios/chrome/browser/snapshots/model/legacy_snapshot_manager.h"
#import "ios/chrome/browser/snapshots/model/model_swift.h"
#import "ios/chrome/browser/snapshots/model/snapshot_id_wrapper.h"
#import "ios/chrome/browser/snapshots/model/snapshot_storage_wrapper.h"
#import "ios/chrome/browser/snapshots/model/web_state_snapshot_info.h"
#import "ios/web/public/web_state.h"

namespace {
// Possible results of snapshotting when the page has been loaded. These values
// are persisted to logs. Entries should not be renumbered and numeric values
// should never be reused.
enum class PageLoadedSnapshotResult {
  // Snapshot was not attempted, since the loading page will result in a stale
  // snapshot.
  kSnapshotNotAttemptedBecausePageLoadFailed = 0,
  // Snapshot was attempted, but the image is either the default image or nil.
  kSnapshotAttemptedAndFailed = 1,
  // Snapshot successfully taken.
  kSnapshotSucceeded = 2,
  // kMaxValue should share the value of the highest enumerator.
  kMaxValue = kSnapshotSucceeded,
};

// Generates an ID for WebState's snapshot.
SnapshotID GenerateSnapshotID(const web::WebState* web_state) {
  DCHECK(web_state->GetUniqueIdentifier().valid());
  DCHECK_GT(web_state->GetUniqueIdentifier().identifier(), 0);

  static_assert(sizeof(decltype(web::WebStateID().identifier())) ==
                sizeof(int32_t));
  return SnapshotID(web_state->GetUniqueIdentifier().identifier());
}

}  // namespace

SnapshotTabHelper::~SnapshotTabHelper() {
  DCHECK(!web_state_);
}

void SnapshotTabHelper::SetDelegate(id<SnapshotGeneratorDelegate> delegate) {
  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    CHECK(snapshot_manager_);
    [snapshot_manager_ setDelegate:delegate];
  } else {
    CHECK(legacy_snapshot_manager_);
    [legacy_snapshot_manager_ setDelegate:delegate];
  }
}

void SnapshotTabHelper::SetSnapshotStorage(SnapshotStorageWrapper* wrapper) {
  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    CHECK(snapshot_manager_);
    SnapshotStorage* storage = nil;
    // `wrapper` is nil when a WebState is detached.
    if (wrapper) {
      storage = wrapper.snapshotStorage;
      CHECK(storage);
    }
    // Note that `snapshot_manager_.snapshotStorage` is SnapshotStorage. On the
    // other hand, `legacy_snapshot_manager_.snapshotStorage` is
    // SnapshotStorageWrapper. The type is different to avoid a dependency
    // cycle.
    snapshot_manager_.snapshotStorage = storage;
  } else {
    CHECK(legacy_snapshot_manager_);
    legacy_snapshot_manager_.snapshotStorage = wrapper;
  }
}

void SnapshotTabHelper::RetrieveColorSnapshot(void (^callback)(UIImage*)) {
  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    CHECK(snapshot_manager_);
    [snapshot_manager_ retrieveSnapshotWithCompletion:callback];
  } else {
    CHECK(legacy_snapshot_manager_);
    [legacy_snapshot_manager_ retrieveSnapshot:callback];
  }
}

void SnapshotTabHelper::RetrieveGreySnapshot(void (^callback)(UIImage*)) {
  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    CHECK(snapshot_manager_);
    [snapshot_manager_ retrieveGreySnapshotWithCompletion:callback];
  } else {
    CHECK(legacy_snapshot_manager_);
    [legacy_snapshot_manager_ retrieveGreySnapshot:callback];
  }
}

void SnapshotTabHelper::UpdateSnapshotWithCallback(void (^callback)(UIImage*)) {
  was_loading_during_last_snapshot_ = web_state_->IsLoading();

  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    CHECK(snapshot_manager_);
    [snapshot_manager_ updateSnapshotWithCompletion:callback];
  } else {
    CHECK(legacy_snapshot_manager_);
    [legacy_snapshot_manager_ updateSnapshotWithCompletion:callback];
  }
}

UIImage* SnapshotTabHelper::GenerateSnapshotWithoutOverlays() {
  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    CHECK(snapshot_manager_);
    return [snapshot_manager_ generateUIViewSnapshot];
  } else {
    CHECK(legacy_snapshot_manager_);
    return [legacy_snapshot_manager_ generateUIViewSnapshot];
  }
}

void SnapshotTabHelper::RemoveSnapshot() {
  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    CHECK(snapshot_manager_);
    [snapshot_manager_ removeSnapshot];
  } else {
    CHECK(legacy_snapshot_manager_);
    [legacy_snapshot_manager_ removeSnapshot];
  }
}

void SnapshotTabHelper::IgnoreNextLoad() {
  ignore_next_load_ = true;
}

SnapshotID SnapshotTabHelper::GetSnapshotID() const {
  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    CHECK(snapshot_manager_);
    return snapshot_manager_.snapshotID.snapshot_id;
  } else {
    CHECK(legacy_snapshot_manager_);
    return legacy_snapshot_manager_.snapshotID;
  }
}

SnapshotTabHelper::SnapshotTabHelper(web::WebState* web_state)
    : web_state_(web_state) {
  DCHECK(web_state_);
  if (base::FeatureList::IsEnabled(kSnapshotInSwift)) {
    snapshot_manager_ = [[SnapshotManager alloc]
        initWithGenerator:
            [[SnapshotGenerator alloc]
                initWithWebStateInfo:[[WebStateSnapshotInfo alloc]
                                         initWithWebState:web_state_]]
               snapshotID:[[SnapshotIDWrapper alloc]
                              initWithSnapshotID:GenerateSnapshotID(
                                                     web_state_)]];
  } else {
    legacy_snapshot_manager_ = [[LegacySnapshotManager alloc]
        initWithGenerator:[[LegacySnapshotGenerator alloc]
                              initWithWebState:web_state_]
               snapshotID:GenerateSnapshotID(web_state_)];
  }

  web_state_observation_.Observe(web_state_.get());
}

void SnapshotTabHelper::PageLoaded(
    web::WebState* web_state,
    web::PageLoadCompletionStatus load_completion_status) {
  // Snapshots taken while page is loading will eventually be stale. It
  // is important that another snapshot is taken after the new
  // page has loaded to replace the stale snapshot. The
  // `IOS.PageLoadedSnapshotResult` histogram shows the outcome of
  // snapshot attempts when the page is loaded after having taken
  // a stale snapshot.
  switch (load_completion_status) {
    case web::PageLoadCompletionStatus::FAILURE:
      // Only log histogram for when a stale snapshot needs to be replaced.
      if (was_loading_during_last_snapshot_) {
        base::UmaHistogramEnumeration(
            "IOS.PageLoadedSnapshotResult",
            PageLoadedSnapshotResult::
                kSnapshotNotAttemptedBecausePageLoadFailed);
      }
      break;

    case web::PageLoadCompletionStatus::SUCCESS:
      if (ignore_next_load_) {
        break;
      }

      bool was_loading = was_loading_during_last_snapshot_;
      base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(
              &SnapshotTabHelper::UpdateSnapshotWithCallback,
              weak_ptr_factory_.GetWeakPtr(),
              ^(UIImage* snapshot) {
                // Only log histogram for when a stale snapshot needs to be
                // replaced.
                if (!was_loading) {
                  return;
                }
                base::UmaHistogramEnumeration(
                    "IOS.PageLoadedSnapshotResult",
                    snapshot ? PageLoadedSnapshotResult::kSnapshotSucceeded
                             : PageLoadedSnapshotResult::
                                   kSnapshotAttemptedAndFailed);
              }),
          base::Seconds(1));
      break;
  }
  ignore_next_load_ = false;
  was_loading_during_last_snapshot_ = false;
}

void SnapshotTabHelper::WebStateDestroyed(web::WebState* web_state) {
  DCHECK_EQ(web_state_, web_state);
  DCHECK(web_state_observation_.IsObservingSource(web_state));
  web_state_observation_.Reset();
  web_state_ = nullptr;
}

WEB_STATE_USER_DATA_KEY_IMPL(SnapshotTabHelper)