chromium/ios/chrome/browser/web/model/page_placeholder_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/web/model/page_placeholder_tab_helper.h"

#import "base/check_op.h"
#import "base/functional/bind.h"
#import "base/task/single_thread_task_runner.h"
#import "base/time/time.h"
#import "ios/chrome/browser/shared/ui/util/named_guide.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"

namespace {
// Placeholder will not be displayed longer than this time.
constexpr base::TimeDelta kPlaceholderMaxDisplayTime = base::Seconds(1.5);

// Placeholder removal will include a fade-out animation of this length.
const NSTimeInterval kPlaceholderFadeOutAnimationLengthInSeconds = 0.5;
}  // namespace

PagePlaceholderTabHelper::PagePlaceholderTabHelper(web::WebState* web_state)
    : web_state_(web_state), weak_factory_(this) {
  web_state_->AddObserver(this);
}

PagePlaceholderTabHelper::~PagePlaceholderTabHelper() {
  RemovePlaceholder();
}

void PagePlaceholderTabHelper::AddPlaceholderForNextNavigation() {
  add_placeholder_for_next_navigation_ = true;
}

void PagePlaceholderTabHelper::CancelPlaceholderForNextNavigation() {
  add_placeholder_for_next_navigation_ = false;
  if (displaying_placeholder_) {
    RemovePlaceholder();
  }
}

void PagePlaceholderTabHelper::WasShown(web::WebState* web_state) {
  if (add_placeholder_for_next_navigation_) {
    add_placeholder_for_next_navigation_ = false;
    AddPlaceholder();
  }
}

void PagePlaceholderTabHelper::WasHidden(web::WebState* web_state) {
  RemovePlaceholder();
}

void PagePlaceholderTabHelper::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  DCHECK_EQ(web_state_, web_state);
  if (add_placeholder_for_next_navigation_ && web_state->IsVisible()) {
    add_placeholder_for_next_navigation_ = false;
    AddPlaceholder();
  }
}

void PagePlaceholderTabHelper::PageLoaded(web::WebState* web_state,
                                          web::PageLoadCompletionStatus) {
  DCHECK_EQ(web_state_, web_state);
  RemovePlaceholder();
}

void PagePlaceholderTabHelper::WebStateDestroyed(web::WebState* web_state) {
  DCHECK_EQ(web_state_, web_state);
  web_state_->RemoveObserver(this);
  web_state_ = nullptr;
  RemovePlaceholder();
}

void PagePlaceholderTabHelper::OnImageRetrieved(UIImage* image) {
  if (displaying_placeholder()) {
    DisplaySnapshotImage(image);
  }
}

void PagePlaceholderTabHelper::AddPlaceholder() {
  // WebState::WasShown() and WebState::IsVisible() are bookkeeping mechanisms
  // that do not guarantee the WebState's view is in the view hierarchy.
  // TODO(crbug.com/40630853): Do not DCHECK([webState->GetView() window]) here
  // since this is a known issue.
  if (displaying_placeholder_ || ![web_state_->GetView() window])
    return;

  displaying_placeholder_ = true;

  // Lazily create the placeholder view.
  if (!placeholder_view_) {
    placeholder_view_ = [[TopAlignedImageView alloc] init];
    placeholder_view_.backgroundColor = [UIColor whiteColor];
    placeholder_view_.translatesAutoresizingMaskIntoConstraints = NO;
  }

  // Update placeholder view's image and display it on top of WebState's view.
  SnapshotTabHelper* snapshotTabHelper =
      SnapshotTabHelper::FromWebState(web_state_);
  if (snapshotTabHelper) {
    // Show grey snapshots only for the WebStates that haven't been loaded
    if (web_state_->IsLoading()) {
      snapshotTabHelper->RetrieveGreySnapshot(base::CallbackToBlock(
          base::BindOnce(&PagePlaceholderTabHelper::OnImageRetrieved,
                         weak_factory_.GetWeakPtr())));
    } else {
      snapshotTabHelper->RetrieveColorSnapshot(base::CallbackToBlock(
          base::BindOnce(&PagePlaceholderTabHelper::OnImageRetrieved,
                         weak_factory_.GetWeakPtr())));
    }
  }

  // Remove placeholder if it takes too long to load the page.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&PagePlaceholderTabHelper::RemovePlaceholder,
                     weak_factory_.GetWeakPtr()),
      kPlaceholderMaxDisplayTime);
}

void PagePlaceholderTabHelper::DisplaySnapshotImage(UIImage* snapshot) {
  UIView* web_state_view = web_state_->GetView();
  NamedGuide* guide = [NamedGuide guideWithName:kContentAreaGuide
                                           view:web_state_view];

  // TODO(crbug.com/40630853): It is a known issue that the guide may be nil,
  // causing a crash when attempting to display the placeholder. Choose to not
  // display the placeholder rather than crashing, since the placeholder is not
  // critical to the user experience.
  if (!guide) {
    displaying_placeholder_ = false;
    return;
  }
  placeholder_view_.image = snapshot;
  [web_state_view addSubview:placeholder_view_];
  AddSameConstraints(guide, placeholder_view_);
}

void PagePlaceholderTabHelper::RemovePlaceholder() {
  if (!displaying_placeholder_)
    return;

  displaying_placeholder_ = false;

  // Remove placeholder view with a fade-out animation.
  __weak UIView* weak_placeholder_view = placeholder_view_;
  [UIView animateWithDuration:kPlaceholderFadeOutAnimationLengthInSeconds
      animations:^{
        weak_placeholder_view.alpha = 0.0f;
      }
      completion:^(BOOL finished) {
        [weak_placeholder_view removeFromSuperview];
        weak_placeholder_view.alpha = 1.0f;
      }];
}

WEB_STATE_USER_DATA_KEY_IMPL(PagePlaceholderTabHelper)