chromium/ios/chrome/browser/web/model/annotations/annotations_tab_helper.mm

// 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.

#import "ios/chrome/browser/web/model/annotations/annotations_tab_helper.h"

#import "base/apple/foundation_util.h"
#import "base/containers/contains.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/task/thread_pool.h"
#import "base/uuid.h"
#import "base/values.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#import "ios/chrome/browser/mailto_handler/model/mailto_handler_service.h"
#import "ios/chrome/browser/mailto_handler/model/mailto_handler_service_factory.h"
#import "ios/chrome/browser/parcel_tracking/features.h"
#import "ios/chrome/browser/parcel_tracking/parcel_tracking_prefs.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/parcel_tracking_opt_in_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/text_selection/model/text_classifier_model_service.h"
#import "ios/chrome/browser/text_selection/model/text_classifier_model_service_factory.h"
#import "ios/public/provider/chrome/browser/context_menu/context_menu_api.h"
#import "ios/web/common/annotations_utils.h"
#import "ios/web/common/features.h"
#import "ios/web/common/url_scheme_util.h"
#import "ios/web/public/annotations/annotations_text_manager.h"
#import "ios/web/public/browser_state.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/web_state.h"

AnnotationsTabHelper::AnnotationsTabHelper(web::WebState* web_state)
    : web_state_(web_state) {
  DCHECK(web_state_);
  web_state_->AddObserver(this);
  // In some cases, AnnotationsTextManager is created before this and in some
  // others after. Make sure it exists.
  web::AnnotationsTextManager::CreateForWebState(web_state);
  auto* manager = web::AnnotationsTextManager::FromWebState(web_state);
  manager->AddObserver(this);
  manager->SetSupportedTypes(
      ios::provider::GetHandledIntentTypesForOneTap(web_state));
}

AnnotationsTabHelper::~AnnotationsTabHelper() {
  web_state_ = nullptr;
}

void AnnotationsTabHelper::SetBaseViewController(
    UIViewController* base_view_controller) {
  base_view_controller_ = base_view_controller;
}

void AnnotationsTabHelper::SetMiniMapCommands(
    id<MiniMapCommands> mini_map_handler) {
  mini_map_handler_ = mini_map_handler;
}

void AnnotationsTabHelper::SetParcelTrackingOptInCommands(
    id<ParcelTrackingOptInCommands> parcel_tracking_handler) {
  parcel_tracking_handler_ = parcel_tracking_handler;
}

void AnnotationsTabHelper::SetUnitConversionCommands(
    id<UnitConversionCommands> unit_conversion_handler) {
  unit_conversion_handler_ = unit_conversion_handler;
}

#pragma mark - WebStateObserver methods.

void AnnotationsTabHelper::WebStateDestroyed(web::WebState* web_state) {
  web_state_->RemoveObserver(this);
  auto* manager = web::AnnotationsTextManager::FromWebState(web_state);
  manager->RemoveObserver(this);
  web_state_ = nullptr;
}

void AnnotationsTabHelper::PageLoaded(
    web::WebState* web_state,
    web::PageLoadCompletionStatus load_completion_status) {
  DCHECK_EQ(web_state_, web_state);
  if (load_completion_status == web::PageLoadCompletionStatus::SUCCESS) {
    match_cache_.clear();
  }
}

#pragma mark - AnnotationsTextObserver methods.

void AnnotationsTabHelper::OnTextExtracted(web::WebState* web_state,
                                           const std::string& text,
                                           int seq_id,
                                           const base::Value::Dict& metadata) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_EQ(web_state_, web_state);

  if (!base::FeatureList::IsEnabled(web::features::kEnableViewportIntents)) {
    // Check if this page requested "nointentdetection".
    std::optional<bool> has_no_intent_detection =
        metadata.FindBool("hasNoIntentDetection");
    if (!has_no_intent_detection || has_no_intent_detection.value()) {
      return;
    }
  }

  NSTextCheckingType handled_types =
      ios::provider::GetHandledIntentTypesForOneTap(web_state);
  std::optional<bool> has_no_telephone = metadata.FindBool("wkNoTelephone");
  if (has_no_telephone && has_no_telephone.value()) {
    handled_types = handled_types & ~NSTextCheckingTypePhoneNumber;
  }
  std::optional<bool> has_no_email = metadata.FindBool("wkNoEmail");
  if (has_no_email && has_no_email.value()) {
    handled_types = handled_types & ~NSTextCheckingTypeLink;
  }
  std::optional<bool> has_no_address = metadata.FindBool("wkNoAddress");
  if (has_no_address && has_no_address.value()) {
    handled_types = handled_types & ~NSTextCheckingTypeAddress;
  }
  std::optional<bool> has_no_date = metadata.FindBool("wkNoDate");
  if (has_no_date && has_no_date.value()) {
    handled_types = handled_types & ~NSTextCheckingTypeDate;
  }
  std::optional<bool> has_no_unit = metadata.FindBool("wkNoUnit");
  if (has_no_unit && has_no_unit.value()) {
    handled_types = handled_types & ~TCTextCheckingTypeMeasurement;
  }

  // Keep latest copy.
  metadata_ = std::make_unique<base::Value::Dict>(metadata.Clone());

  TextClassifierModelService* service =
      TextClassifierModelServiceFactory::GetForBrowserState(
          ChromeBrowserState::FromBrowserState(web_state->GetBrowserState()));
  base::FilePath model_path =
      service ? service->GetModelPath() : base::FilePath();

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
      base::BindOnce(&ios::provider::ExtractTextAnnotationFromText,
                     metadata.Clone(), text, handled_types,
                     ukm::GetSourceIdForWebStateDocument(web_state),
                     std::move(model_path)),
      base::BindOnce(&AnnotationsTabHelper::ApplyDeferredProcessing,
                     weak_factory_.GetWeakPtr(), seq_id));
}

void AnnotationsTabHelper::OnDecorated(web::WebState* web_state,
                                       int annotations,
                                       int successes,
                                       int failures,
                                       const base::Value::List& cancelled) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (annotations) {
    int percentage = (100 * successes) / annotations;
    base::UmaHistogramPercentage("IOS.Annotations.Percentage", percentage);
  }
  for (size_t i = 0; i < cancelled.size(); i++) {
    const std::string* cancelledId = cancelled[i].GetIfString();
    if (!cancelledId || match_cache_.find(*cancelledId) == match_cache_.end()) {
      continue;
    }
    match_cache_.erase(*cancelledId);
  }
}

void AnnotationsTabHelper::OnClick(web::WebState* web_state,
                                   const std::string& text,
                                   CGRect rect,
                                   const std::string& data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (match_cache_.find(data) == match_cache_.end()) {
    return;
  }
  NSTextCheckingResult* match = match_cache_.at(data);
  auto* manager = web::AnnotationsTextManager::FromWebState(web_state_);
  if (manager) {
    manager->RemoveHighlight();
  }

  NSString* ns_text = base::SysUTF8ToNSString(text);
  const BOOL success = ios::provider::HandleIntentTypesForOneTap(
      web_state, match, ns_text, rect.origin, base_view_controller_,
      mini_map_handler_, unit_conversion_handler_);
  DCHECK(success);
}

#pragma mark - Private Methods

void AnnotationsTabHelper::ApplyDeferredProcessing(
    int seq_id,
    std::optional<std::vector<web::TextAnnotation>> deferred) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto* manager = web::AnnotationsTextManager::FromWebState(web_state_);
  DCHECK(manager);

  if (!deferred &&
      base::FeatureList::IsEnabled(web::features::kEnableViewportIntents)) {
    base::Value::List decorations_list;
    base::Value decorations(std::move(decorations_list));
    manager->DecorateAnnotations(web_state_, decorations, seq_id);
    return;
  }

  web::ContentWorld content_world =
      web::AnnotationsTextManager::GetFeatureContentWorld();
  web::WebFrame* main_frame =
      web_state_->GetWebFramesManager(content_world)->GetMainWebFrame();
  if (main_frame && deferred) {
    std::vector<web::TextAnnotation> annotations(std::move(deferred.value()));

    PrefService* prefs = IsHomeCustomizationEnabled()
                             ? ChromeBrowserState::FromBrowserState(
                                   web_state_->GetBrowserState())
                                   ->GetPrefs()
                             : GetApplicationContext()->GetLocalState();

    if (IsIOSParcelTrackingEnabled() && !IsParcelTrackingDisabled(prefs)) {
      parcel_number_tracker_.ProcessAnnotations(annotations);
      // Show UI only if this is the currently active WebState.
      if (parcel_number_tracker_.HasNewTrackingNumbers() &&
          web_state_->IsVisible()) {
        // Call asynchronously to allow the rest of the annotations to be
        // decorated first.
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(&AnnotationsTabHelper::MaybeShowParcelTrackingUI,
                           weak_factory_.GetWeakPtr(),
                           parcel_number_tracker_.GetNewTrackingNumbers()));
      }
    }
    if (base::FeatureList::IsEnabled(web::features::kEnableMeasurements)) {
      ProcessAnnotations(annotations);
    }
    base::Value::List decorations_list;
    BuildCacheAndDecorations(annotations, decorations_list);
    base::Value decorations(std::move(decorations_list));
    manager->DecorateAnnotations(web_state_, decorations, seq_id);
  }
}

void AnnotationsTabHelper::BuildCacheAndDecorations(
    std::vector<web::TextAnnotation>& annotations_list,
    base::Value::List& decorations) {
  for (web::TextAnnotation& data : annotations_list) {
    const std::string key = base::Uuid::GenerateRandomV4().AsLowercaseString();
    match_cache_[key] = data.second;
    data.first.Set("data", key);
    decorations.Append(base::Value(std::move(data.first)));
  }
}

void AnnotationsTabHelper::ProcessAnnotations(
    std::vector<web::TextAnnotation>& annotations_list) {
  if (!base::FeatureList::IsEnabled(web::features::kEnableMeasurements)) {
    return;
  }
  int detected_measurements = 0;
  for (auto annotation = annotations_list.begin();
       annotation != annotations_list.end();) {
    NSTextCheckingResult* match = annotation->second;
    if (match && match.resultType == TCTextCheckingTypeMeasurement) {
      detected_measurements++;
    }
    annotation++;
  }
  base::UmaHistogramCounts100("IOS.UnitConversion.DetectedMeasurements",
                              detected_measurements);
}

void AnnotationsTabHelper::MaybeShowParcelTrackingUI(
    NSArray<CustomTextCheckingResult*>* parcels) {
  [parcel_tracking_handler_ showTrackingForParcels:parcels];
}

WEB_STATE_USER_DATA_KEY_IMPL(AnnotationsTabHelper)