| // 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/ui/content_suggestions/magic_stack/magic_stack_ranking_model.h" |
| |
| #import <optional> |
| |
| #import "base/check.h" |
| #import "base/ios/block_types.h" |
| #import "base/memory/raw_ptr.h" |
| #import "base/metrics/histogram_functions.h" |
| #import "base/metrics/histogram_macros.h" |
| #import "components/commerce/core/commerce_feature_list.h" |
| #import "components/commerce/core/shopping_service.h" |
| #import "components/password_manager/core/common/password_manager_pref_names.h" |
| #import "components/prefs/pref_service.h" |
| #import "components/safe_browsing/core/common/safe_browsing_prefs.h" |
| #import "components/search/search.h" |
| #import "components/search_engines/template_url_service.h" |
| #import "components/segmentation_platform/embedder/home_modules/address_bar_position_ephemeral_module.h" |
| #import "components/segmentation_platform/embedder/home_modules/autofill_passwords_ephemeral_module.h" |
| #import "components/segmentation_platform/embedder/home_modules/constants.h" |
| #import "components/segmentation_platform/embedder/home_modules/enhanced_safe_browsing_ephemeral_module.h" |
| #import "components/segmentation_platform/embedder/home_modules/home_modules_card_registry.h" |
| #import "components/segmentation_platform/embedder/home_modules/lens_ephemeral_module.h" |
| #import "components/segmentation_platform/embedder/home_modules/save_passwords_ephemeral_module.h" |
| #import "components/segmentation_platform/embedder/home_modules/send_tab_notification_promo.h" |
| #import "components/segmentation_platform/embedder/home_modules/tips_manager/constants.h" |
| #import "components/segmentation_platform/embedder/home_modules/tips_manager/signal_constants.h" |
| #import "components/segmentation_platform/public/constants.h" |
| #import "components/segmentation_platform/public/features.h" |
| #import "components/segmentation_platform/public/segmentation_platform_service.h" |
| #import "components/send_tab_to_self/features.h" |
| #import "components/send_tab_to_self/pref_names.h" |
| #import "ios/chrome/browser/lens/ui_bundled/lens_availability.h" |
| #import "ios/chrome/browser/lens/ui_bundled/lens_entrypoint.h" |
| #import "ios/chrome/browser/ntp/model/features.h" |
| #import "ios/chrome/browser/ntp/ui_bundled/home_start_data_source.h" |
| #import "ios/chrome/browser/ntp_tiles/model/tab_resumption/tab_resumption_prefs.h" |
| #import "ios/chrome/browser/parcel_tracking/features.h" |
| #import "ios/chrome/browser/parcel_tracking/parcel_tracking_prefs.h" |
| #import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager_constants.h" |
| #import "ios/chrome/browser/shared/model/application_context/application_context.h" |
| #import "ios/chrome/browser/shared/model/prefs/pref_names.h" |
| #import "ios/chrome/browser/shared/model/utils/first_run_util.h" |
| #import "ios/chrome/browser/shared/public/features/features.h" |
| #import "ios/chrome/browser/shared/public/features/system_flags.h" |
| #import "ios/chrome/browser/tips_manager/model/tips_manager_ios.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/most_visited_tiles_config.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/most_visited_tiles_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/shortcuts_config.h" |
| #import "ios/chrome/browser/ui/content_suggestions/cells/shortcuts_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_constants.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_constants.h" |
| #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_recorder.h" |
| #import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_ranking_model_delegate.h" |
| #import "ios/chrome/browser/ui/content_suggestions/magic_stack/magic_stack_utils.h" |
| #import "ios/chrome/browser/ui/content_suggestions/parcel_tracking/parcel_tracking_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/parcel_tracking/parcel_tracking_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/price_tracking_promo/price_tracking_promo_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/price_tracking_promo/price_tracking_promo_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_magic_stack_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_prefs.h" |
| #import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_state.h" |
| #import "ios/chrome/browser/ui/content_suggestions/send_tab_to_self/send_tab_promo_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/send_tab_to_self/send_tab_promo_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/set_up_list/set_up_list_config.h" |
| #import "ios/chrome/browser/ui/content_suggestions/set_up_list/set_up_list_item_view_data.h" |
| #import "ios/chrome/browser/ui/content_suggestions/set_up_list/set_up_list_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/set_up_list/utils.h" |
| #import "ios/chrome/browser/ui/content_suggestions/shop_card/shop_card_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/shop_card/shop_card_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_helper_delegate.h" |
| #import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_item.h" |
| #import "ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/tips/tips_magic_stack_mediator.h" |
| #import "ios/chrome/browser/ui/content_suggestions/tips/tips_module_state.h" |
| #import "ios/chrome/browser/ui/content_suggestions/tips/tips_prefs.h" |
| #import "ui/base/device_form_factor.h" |
| |
| using segmentation_platform::TipIdentifier; |
| using segmentation_platform::TipIdentifierForOutputLabel; |
| using segmentation_platform::home_modules::AddressBarPositionEphemeralModule; |
| using segmentation_platform::home_modules::AutofillPasswordsEphemeralModule; |
| using segmentation_platform::home_modules::EnhancedSafeBrowsingEphemeralModule; |
| using segmentation_platform::home_modules::LensEphemeralModule; |
| using segmentation_platform::home_modules::SavePasswordsEphemeralModule; |
| |
| @interface MagicStackRankingModel () <MostVisitedTilesMediatorDelegate, |
| ParcelTrackingMediatorDelegate, |
| PriceTrackingPromoMediatorDelegate, |
| SafetyCheckMagicStackMediatorDelegate, |
| SendTabPromoMediatorDelegate, |
| ShopCardMediatorDelegate, |
| SetUpListMediatorAudience, |
| ShortcutsMediatorDelegate, |
| TabResumptionHelperDelegate, |
| TipsMagicStackMediatorDelegate> |
| // For testing-only |
| @property(nonatomic, assign) BOOL hasReceivedMagicStackResponse; |
| @property(nonatomic, assign) BOOL hasReceivedEphemericalCardResponse; |
| @end |
| |
| @implementation MagicStackRankingModel { |
| raw_ptr<segmentation_platform::SegmentationPlatformService> |
| _segmentationService; |
| raw_ptr<commerce::ShoppingService> _shoppingService; |
| raw_ptr<AuthenticationService> _authService; |
| raw_ptr<PrefService> _prefService; |
| raw_ptr<PrefService> _localState; |
| // The latest module ranking returned from the SegmentationService. |
| NSArray<NSNumber*>* _magicStackOrderFromSegmentation; |
| // YES if the module ranking has been received from the SegmentationService. |
| BOOL _magicStackOrderFromSegmentationReceived; |
| // The latest Magic Stack module order sent up to the consumer. This includes |
| // any omissions due to filtering from `_magicStackOrderFromSegmentation` and |
| // any additions beyond `_magicStackOrderFromSegmentation` (e.g. Set Up List). |
| NSArray<MagicStackModule*>* _latestMagicStackConfigOrder; |
| // Module mediators. |
| MostVisitedTilesMediator* _mostVisitedTilesMediator; |
| SetUpListMediator* _setUpListMediator; |
| TabResumptionMediator* _tabResumptionMediator; |
| ParcelTrackingMediator* _parcelTrackingMediator; |
| PriceTrackingPromoMediator* _priceTrackingPromoMediator; |
| ShopCardMediator* _shopCardMediator; |
| ShortcutsMediator* _shortcutsMediator; |
| SafetyCheckMagicStackMediator* _safetyCheckMediator; |
| SendTabPromoMediator* _sendTabPromoMediator; |
| TipsMagicStackMediator* _tipsMediator; |
| raw_ptr<TipsManagerIOS> _tipsManager; |
| base::TimeTicks ranking_fetch_start_time_; |
| ContentSuggestionsModuleType _ephemeralCardToShow; |
| raw_ptr<TemplateURLService> _templateURLService; |
| } |
| |
| - (instancetype) |
| initWithSegmentationService: |
| (segmentation_platform::SegmentationPlatformService*)segmentationService |
| shoppingService:(commerce::ShoppingService*)shoppingService |
| authService:(AuthenticationService*)authenticationService |
| prefService:(PrefService*)prefService |
| localState:(PrefService*)localState |
| moduleMediators:(NSArray*)moduleMediators |
| tipsManager:(TipsManagerIOS*)tipsManager |
| templateURLService:(TemplateURLService*)templateURLService { |
| self = [super init]; |
| if (self) { |
| _segmentationService = segmentationService; |
| _shoppingService = shoppingService; |
| _authService = authenticationService; |
| _prefService = prefService; |
| _localState = localState; |
| _ephemeralCardToShow = ContentSuggestionsModuleType::kInvalid; |
| _templateURLService = templateURLService; |
| |
| if (IsTipsMagicStackEnabled()) { |
| CHECK(tipsManager); |
| _tipsManager = tipsManager; |
| } |
| |
| for (id mediator in moduleMediators) { |
| if ([mediator isKindOfClass:[MostVisitedTilesMediator class]]) { |
| _mostVisitedTilesMediator = |
| static_cast<MostVisitedTilesMediator*>(mediator); |
| _mostVisitedTilesMediator.delegate = self; |
| } else if ([mediator isKindOfClass:[SetUpListMediator class]]) { |
| _setUpListMediator = static_cast<SetUpListMediator*>(mediator); |
| _setUpListMediator.audience = self; |
| } else if ([mediator isKindOfClass:[TabResumptionMediator class]]) { |
| _tabResumptionMediator = static_cast<TabResumptionMediator*>(mediator); |
| _tabResumptionMediator.delegate = self; |
| } else if ([mediator isKindOfClass:[ShopCardMediator class]]) { |
| _shopCardMediator = static_cast<ShopCardMediator*>(mediator); |
| _shopCardMediator.delegate = self; |
| } else if ([mediator isKindOfClass:[ShortcutsMediator class]]) { |
| _shortcutsMediator = static_cast<ShortcutsMediator*>(mediator); |
| _shortcutsMediator.delegate = self; |
| } else if ([mediator isKindOfClass:[ParcelTrackingMediator class]]) { |
| _parcelTrackingMediator = |
| static_cast<ParcelTrackingMediator*>(mediator); |
| _parcelTrackingMediator.delegate = self; |
| } else if ([mediator isKindOfClass:[PriceTrackingPromoMediator class]]) { |
| _priceTrackingPromoMediator = |
| static_cast<PriceTrackingPromoMediator*>(mediator); |
| _priceTrackingPromoMediator.delegate = self; |
| } else if ([mediator |
| isKindOfClass:[SafetyCheckMagicStackMediator class]]) { |
| _safetyCheckMediator = |
| static_cast<SafetyCheckMagicStackMediator*>(mediator); |
| _safetyCheckMediator.delegate = self; |
| } else if ([mediator isKindOfClass:[TipsMagicStackMediator class]]) { |
| _tipsMediator = static_cast<TipsMagicStackMediator*>(mediator); |
| _tipsMediator.delegate = self; |
| } else if ([mediator isKindOfClass:[SendTabPromoMediator class]]) { |
| _sendTabPromoMediator = static_cast<SendTabPromoMediator*>(mediator); |
| _sendTabPromoMediator.delegate = self; |
| } else { |
| // Known module mediators need to be handled. |
| NOTREACHED(); |
| } |
| } |
| } |
| return self; |
| } |
| |
| - (void)disconnect { |
| _mostVisitedTilesMediator = nil; |
| _setUpListMediator = nil; |
| _tabResumptionMediator = nil; |
| _parcelTrackingMediator = nil; |
| _priceTrackingPromoMediator = nil; |
| _shortcutsMediator = nil; |
| _safetyCheckMediator = nil; |
| _sendTabPromoMediator = nil; |
| _shopCardMediator = nil; |
| _tipsMediator = nil; |
| _tipsManager = nil; |
| } |
| |
| #pragma mark - Public |
| |
| - (void)fetchLatestMagicStackRanking { |
| _magicStackOrderFromSegmentationReceived = NO; |
| _magicStackOrderFromSegmentation = nil; |
| _latestMagicStackConfigOrder = nil; |
| if (base::FeatureList::IsEnabled( |
| segmentation_platform::features:: |
| kSegmentationPlatformEphemeralCardRanker)) { |
| _ephemeralCardToShow = ContentSuggestionsModuleType::kInvalid; |
| [self fetchEphemeralCardFromSegmentationPlatform]; |
| } |
| [self fetchMagicStackModuleRankingFromSegmentationPlatform]; |
| } |
| |
| - (void)logMagicStackEngagementForType:(ContentSuggestionsModuleType)type { |
| [self.contentSuggestionsMetricsRecorder |
| recordMagicStackModuleEngagementForType:type |
| atIndex: |
| [self indexForMagicStackModule:type]]; |
| } |
| |
| #pragma mark - SetUpListMediatorAudience |
| |
| - (void)removeSetUpList { |
| base::UmaHistogramEnumeration( |
| kMagicStackModuleDisabledHistogram, |
| ContentSuggestionsModuleType::kCompactedSetUpList); |
| [self.delegate magicStackRankingModel:self |
| didRemoveItem:_setUpListMediator.setUpListConfigs[0] |
| animate:YES |
| withCompletion:nil]; |
| } |
| |
| - (void)replaceSetUpListWithAllSet:(SetUpListConfig*)allSetConfig { |
| [self.delegate magicStackRankingModel:self |
| didReplaceItem:_setUpListMediator.setUpListConfigs[0] |
| withItem:allSetConfig]; |
| } |
| |
| #pragma mark - SafetyCheckMagicStackMediatorDelegate |
| |
| - (void)removeSafetyCheckModule { |
| if (![self isMagicStackOrderReady]) { |
| return; |
| } |
| |
| base::UmaHistogramEnumeration(kMagicStackModuleDisabledHistogram, |
| ContentSuggestionsModuleType::kSafetyCheck); |
| [self.delegate magicStackRankingModel:self |
| didRemoveItem:_safetyCheckMediator.safetyCheckState |
| animate:YES |
| withCompletion:nil]; |
| } |
| |
| #pragma mark - SendTabPromoMediatorDelegate |
| |
| - (void)sentTabReceived { |
| MagicStackModule* item = _sendTabPromoMediator.sendTabPromoItemToShow; |
| NSArray<MagicStackModule*>* rank = [self latestMagicStackConfigRank]; |
| NSUInteger index = [rank indexOfObject:item]; |
| if (index == NSNotFound) { |
| return; |
| } |
| [self.delegate magicStackRankingModel:self didInsertItem:item atIndex:index]; |
| } |
| |
| - (void)removeSendTabPromoModule { |
| base::UmaHistogramEnumeration(kMagicStackModuleDisabledHistogram, |
| ContentSuggestionsModuleType::kSendTabPromo); |
| [self.delegate |
| magicStackRankingModel:self |
| didRemoveItem:_sendTabPromoMediator.sendTabPromoItemToShow |
| animate:YES |
| withCompletion:nil]; |
| } |
| |
| #pragma mark - TipsMagicStackMediatorDelegate |
| |
| - (void)removeTipsModuleWithCompletion:(ProceduralBlock)completion { |
| if (![self isMagicStackOrderReady]) { |
| return; |
| } |
| |
| base::UmaHistogramEnumeration(kMagicStackModuleDisabledHistogram, |
| ContentSuggestionsModuleType::kTips); |
| |
| [self.delegate magicStackRankingModel:self |
| didRemoveItem:_tipsMediator.state |
| animate:YES |
| withCompletion:completion]; |
| } |
| |
| #pragma mark - TabResumptionHelperDelegate |
| |
| - (void)tabResumptionHelperDidReceiveItem { |
| CHECK(IsTabResumptionEnabled()); |
| if (tab_resumption_prefs::IsTabResumptionDisabled( |
| IsHomeCustomizationEnabled() ? _prefService : _localState)) { |
| return; |
| } |
| |
| [self showTabResumptionWithItem:_tabResumptionMediator.itemConfig]; |
| } |
| |
| - (void)tabResumptionHelperDidReconfigureItem { |
| if (tab_resumption_prefs::IsTabResumptionDisabled( |
| IsHomeCustomizationEnabled() ? _prefService : _localState)) { |
| return; |
| } |
| TabResumptionItem* item = _tabResumptionMediator.itemConfig; |
| [self.delegate magicStackRankingModel:self didReconfigureItem:item]; |
| } |
| |
| - (void)removeTabResumptionModule { |
| [self.delegate magicStackRankingModel:self |
| didRemoveItem:_tabResumptionMediator.itemConfig |
| animate:NO |
| withCompletion:nil]; |
| } |
| |
| #pragma mark - ParcelTrackingMediatorDelegate |
| |
| - (void)newParcelsAvailable { |
| MagicStackModule* item = _parcelTrackingMediator.parcelTrackingItemToShow; |
| NSArray<MagicStackModule*>* rank = [self latestMagicStackConfigRank]; |
| NSUInteger index = [rank indexOfObject:item]; |
| if (index == NSNotFound) { |
| return; |
| } |
| [self.delegate magicStackRankingModel:self didInsertItem:item atIndex:index]; |
| } |
| |
| - (void)parcelTrackingDisabled { |
| base::UmaHistogramEnumeration(kMagicStackModuleDisabledHistogram, |
| ContentSuggestionsModuleType::kParcelTracking); |
| [self.delegate |
| magicStackRankingModel:self |
| didRemoveItem:_parcelTrackingMediator.parcelTrackingItemToShow |
| animate:YES |
| withCompletion:nil]; |
| } |
| |
| - (NSUInteger)indexForMagicStackModule: |
| (ContentSuggestionsModuleType)moduleType { |
| return [_latestMagicStackConfigOrder |
| indexOfObjectPassingTest:^BOOL(MagicStackModule* config, NSUInteger idx, |
| BOOL* stop) { |
| return config.type == moduleType; |
| }]; |
| } |
| |
| #pragma mark - MostVisitedTilesMediatorDelegate |
| |
| - (void)didReceiveInitialMostVistedTiles { |
| if (![self isMagicStackOrderReady]) { |
| return; |
| } |
| |
| NSArray<MagicStackModule*>* rank = [self latestMagicStackConfigRank]; |
| NSUInteger index = |
| [rank indexOfObject:_mostVisitedTilesMediator.mostVisitedConfig]; |
| [self.delegate |
| magicStackRankingModel:self |
| didInsertItem:_mostVisitedTilesMediator.mostVisitedConfig |
| atIndex:index]; |
| } |
| |
| - (void)removeMostVisitedTilesModule { |
| if (![self isMagicStackOrderReady]) { |
| return; |
| } |
| |
| [self.delegate |
| magicStackRankingModel:self |
| didRemoveItem:_mostVisitedTilesMediator.mostVisitedConfig |
| animate:YES |
| withCompletion:nil]; |
| } |
| |
| #pragma mark - PriceTrackingPromoMediatorDelegate |
| |
| - (void)promoWasTapped { |
| [self logMagicStackEngagementForType:ContentSuggestionsModuleType:: |
| kPriceTrackingPromo]; |
| } |
| |
| #pragma mark - Private |
| |
| // Adds the correct Set Up List module type to the Magic Stack `order`. |
| - (void)addSetUpListToMagicStackOrder:(NSMutableArray*)order { |
| if ([_setUpListMediator allItemsComplete]) { |
| [order addObject:@(int(ContentSuggestionsModuleType::kSetUpListAllSet))]; |
| } else if (set_up_list_utils::ShouldShowCompactedSetUpListModule()) { |
| [order addObject:@(int(ContentSuggestionsModuleType::kCompactedSetUpList))]; |
| } else { |
| for (SetUpListItemViewData* model in _setUpListMediator.setUpListItems) { |
| [order addObject:@(int(SetUpListModuleTypeForSetUpListType(model.type)))]; |
| } |
| } |
| } |
| |
| // Adds the Safety Check module to `order` based on the current Safety Check |
| // state. |
| - (void)addSafetyCheckToMagicStackOrder:(NSMutableArray*)order { |
| CHECK(IsSafetyCheckMagicStackEnabled()); |
| [order addObject:@(int(ContentSuggestionsModuleType::kSafetyCheck))]; |
| } |
| |
| // New subscription observed for user (from another platform). This |
| // has the potential to boost the ranking of the price trackiing promo. |
| - (void)newSubscriptionAvailable { |
| MagicStackModule* item = |
| _priceTrackingPromoMediator.priceTrackingPromoItemToShow; |
| NSArray<MagicStackModule*>* rank = [self latestMagicStackConfigRank]; |
| NSUInteger index = [rank indexOfObject:item]; |
| if (index == NSNotFound) { |
| return; |
| } |
| [self.delegate magicStackRankingModel:self didInsertItem:item atIndex:index]; |
| } |
| |
| // Starts a fetch of the ephemeral card to show from Segmentation. |
| - (void)fetchEphemeralCardFromSegmentationPlatform { |
| segmentation_platform::PredictionOptions options; |
| options.on_demand_execution = true; |
| auto inputContext = |
| base::MakeRefCounted<segmentation_platform::InputContext>(); |
| // This check has to match check in HomeModulesCardRegistry::CreateAllCards() |
| // so that expected inputs match passed inputs. |
| if (base::FeatureList::IsEnabled(commerce::kPriceTrackingPromo) || |
| (IsTipsMagicStackEnabled() && _tipsManager)) { |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kIsNewUser, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| IsFirstRunRecent(set_up_list::SetUpListDurationPastFirstRun()))); |
| } |
| |
| if (base::FeatureList::IsEnabled(commerce::kPriceTrackingPromo)) { |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kIsSynced, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| _shoppingService->IsShoppingListEligible())); |
| } |
| |
| if (send_tab_to_self:: |
| IsSendTabIOSPushNotificationsEnabledWithMagicStackCard()) { |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kSendTabInfobarReceivedInLastSession, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| !_prefService |
| ->GetString(send_tab_to_self::prefs:: |
| kIOSSendTabToSelfLastReceivedTabURLPref) |
| .empty())); |
| } |
| |
| if (IsTipsMagicStackEnabled() && _tipsManager) { |
| // Profile signals |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kLensNotUsedRecently, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| !_tipsManager->WasSignalFiredWithin( |
| segmentation_platform::tips_manager::signals::kLensUsed, |
| base::Days(30)))); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::tips_manager::signals::kOpenedShoppingWebsite, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| _tipsManager->WasSignalFired(segmentation_platform::tips_manager:: |
| signals::kOpenedShoppingWebsite))); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::tips_manager::signals:: |
| kOpenedWebsiteInAnotherLanguage, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| _tipsManager->WasSignalFired( |
| segmentation_platform::tips_manager::signals:: |
| kOpenedWebsiteInAnotherLanguage))); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kNoSavedPasswords, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| !_tipsManager->WasSignalFired(segmentation_platform::tips_manager:: |
| signals::kSavedPasswords))); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::tips_manager::signals::kUsedGoogleTranslation, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| _tipsManager->WasSignalFired(segmentation_platform::tips_manager:: |
| signals::kUsedGoogleTranslation))); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kDidNotUsePasswordAutofill, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| !_tipsManager->WasSignalFired(segmentation_platform::tips_manager:: |
| signals::kUsedPasswordAutofill))); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kLacksEnhancedSafeBrowsing, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| !_prefService->GetBoolean(prefs::kSafeBrowsingEnhanced))); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kPasswordManagerAllowedByEnterprisePolicy, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| _prefService->GetBoolean( |
| password_manager::prefs::kCredentialsEnableService))); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kEnhancedSafeBrowsingAllowedByEnterprisePolicy, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| _prefService->GetBoolean(prefs::kAdvancedProtectionAllowed))); |
| |
| // Local signals |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kDidNotSeeAddressBarPositionChoiceScreen, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| !_tipsManager->WasSignalFired( |
| segmentation_platform::tips_manager::signals:: |
| kAddressBarPositionChoiceScreenDisplayed))); |
| |
| // Miscellaneous signals |
| BOOL isPhone = ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_PHONE; |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kIsPhoneFormFactor, |
| segmentation_platform::processing::ProcessedValue::FromFloat(isPhone)); |
| |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kLensAllowedByEnterprisePolicy, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| [self isLensEnabled])); |
| } |
| |
| __weak MagicStackRankingModel* weakSelf = self; |
| _segmentationService->GetClassificationResult( |
| segmentation_platform::kEphemeralHomeModuleBackendKey, options, |
| inputContext, |
| base::BindOnce( |
| ^(const segmentation_platform::ClassificationResult& result) { |
| weakSelf.hasReceivedEphemericalCardResponse = YES; |
| [weakSelf didReceiveEphemeralCardSegmentationResult:result]; |
| })); |
| } |
| |
| // Handles the ephemeral card Segmentation response and adds a card if there is |
| // one to show. |
| - (void)didReceiveEphemeralCardSegmentationResult: |
| (const segmentation_platform::ClassificationResult&)result { |
| if (result.status != segmentation_platform::PredictionStatus::kSucceeded) { |
| return; |
| } |
| |
| MagicStackModule* card; |
| |
| for (const std::string& label : result.ordered_labels) { |
| if (label == segmentation_platform::kPriceTrackingNotificationPromo) { |
| if (IsPriceTrackingPromoCardEnabled(_shoppingService, _authService, |
| _prefService)) { |
| _ephemeralCardToShow = |
| ContentSuggestionsModuleType::kPriceTrackingPromo; |
| card = _priceTrackingPromoMediator.priceTrackingPromoItemToShow; |
| break; |
| } |
| } else if (segmentation_platform::home_modules::HomeModulesCardRegistry:: |
| IsEphemeralTipsModuleLabel(label) && |
| IsTipsMagicStackEnabled() && |
| !tips_prefs::IsTipsInMagicStackDisabled(_prefService)) { |
| TipIdentifier tipIdentifier = TipIdentifierForOutputLabel(label); |
| |
| if (tipIdentifier != TipIdentifier::kUnknown) { |
| BOOL shouldShowTipsWithProductImage = |
| tipIdentifier == TipIdentifier::kLensShop && |
| TipsLensShopExperimentTypeEnabled() == |
| TipsLensShopExperimentType::kWithProductImage && |
| _tipsMediator.state.productImageData.length > 0; |
| |
| _ephemeralCardToShow = |
| shouldShowTipsWithProductImage |
| ? ContentSuggestionsModuleType::kTipsWithProductImage |
| : ContentSuggestionsModuleType::kTips; |
| |
| [_tipsMediator reconfigureWithTipIdentifier:tipIdentifier]; |
| |
| card = _tipsMediator.state; |
| |
| break; |
| } |
| } else if (label == segmentation_platform::kSendTabNotificationPromo) { |
| if (send_tab_to_self:: |
| IsSendTabIOSPushNotificationsEnabledWithMagicStackCard()) { |
| _ephemeralCardToShow = ContentSuggestionsModuleType::kSendTabPromo; |
| card = _sendTabPromoMediator.sendTabPromoItemToShow; |
| break; |
| } |
| } |
| } |
| if (_ephemeralCardToShow != ContentSuggestionsModuleType::kInvalid && card) { |
| [self addEphemeralCardToMagicStack:card]; |
| } |
| } |
| |
| // Re-calculates the Magic Stack order and inserts the new ephemeral `card` if |
| // the Magic Stack ranking has been received. |
| - (void)addEphemeralCardToMagicStack:(MagicStackModule*)card { |
| if (!_magicStackOrderFromSegmentationReceived) { |
| return; |
| } |
| |
| _latestMagicStackConfigOrder = [self latestMagicStackConfigRank]; |
| [self.delegate magicStackRankingModel:self didInsertItem:card atIndex:0]; |
| } |
| |
| - (void)removePriceTrackingPromo { |
| [self.delegate magicStackRankingModel:self |
| didRemoveItem:_priceTrackingPromoMediator |
| .priceTrackingPromoItemToShow |
| animate:YES |
| withCompletion:nil]; |
| } |
| |
| - (void)removeShopCard { |
| [self.delegate magicStackRankingModel:self |
| didRemoveItem:_shopCardMediator.shopCardItemToShow |
| animate:YES |
| withCompletion:nil]; |
| } |
| |
| // Starts a fetch of the Segmentation module ranking. |
| - (void)fetchMagicStackModuleRankingFromSegmentationPlatform { |
| if (!base::FeatureList::IsEnabled(segmentation_platform::features:: |
| kSegmentationPlatformIosModuleRanker)) { |
| segmentation_platform::ClassificationResult result( |
| segmentation_platform::PredictionStatus::kNotReady); |
| self.hasReceivedMagicStackResponse = YES; |
| [self didReceiveSegmentationServiceResult:result]; |
| return; |
| } |
| auto inputContext = |
| base::MakeRefCounted<segmentation_platform::InputContext>(); |
| if (base::FeatureList::IsEnabled( |
| segmentation_platform::features:: |
| kSegmentationPlatformIosModuleRankerSplitBySurface)) { |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kIsShowingStartSurface, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| [self.homeStartDataSource isStartSurface])); |
| } |
| int mvtFreshnessImpressionCount = _localState->GetInteger( |
| prefs::kIosMagicStackSegmentationMVTImpressionsSinceFreshness); |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kMostVisitedTilesFreshness, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| mvtFreshnessImpressionCount)); |
| int shortcutsFreshnessImpressionCount = _localState->GetInteger( |
| prefs::kIosMagicStackSegmentationShortcutsImpressionsSinceFreshness); |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kShortcutsFreshness, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| shortcutsFreshnessImpressionCount)); |
| int safetyCheckFreshnessImpressionCount = _localState->GetInteger( |
| prefs::kIosMagicStackSegmentationSafetyCheckImpressionsSinceFreshness); |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kSafetyCheckFreshness, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| safetyCheckFreshnessImpressionCount)); |
| int tabResumptionFreshnessImpressionCount = _localState->GetInteger( |
| prefs::kIosMagicStackSegmentationTabResumptionImpressionsSinceFreshness); |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kTabResumptionFreshness, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| tabResumptionFreshnessImpressionCount)); |
| int parcelTrackingFreshnessImpressionCount = _localState->GetInteger( |
| prefs::kIosMagicStackSegmentationParcelTrackingImpressionsSinceFreshness); |
| inputContext->metadata_args.emplace( |
| segmentation_platform::kParcelTrackingFreshness, |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| parcelTrackingFreshnessImpressionCount)); |
| __weak MagicStackRankingModel* weakSelf = self; |
| segmentation_platform::PredictionOptions options; |
| |
| if (base::FeatureList::IsEnabled( |
| kSegmentationPlatformIosModuleRankerCaching)) { |
| // Ignores tab resumption freshness since local tab always logs a freshness |
| // signal for Start. |
| BOOL hasNoFreshnessSignal = shortcutsFreshnessImpressionCount != 0 && |
| parcelTrackingFreshnessImpressionCount != 0; |
| if (IsSafetyCheckMagicStackEnabled()) { |
| hasNoFreshnessSignal = |
| hasNoFreshnessSignal && safetyCheckFreshnessImpressionCount != 0; |
| } |
| if (hasNoFreshnessSignal && [self.homeStartDataSource isStartSurface]) { |
| options = segmentation_platform::PredictionOptions::ForCached(true); |
| } else { |
| options = segmentation_platform::PredictionOptions::ForOnDemand(true); |
| } |
| options.can_update_cache_for_future_requests = true; |
| } else { |
| options.on_demand_execution = true; |
| } |
| ranking_fetch_start_time_ = base::TimeTicks::Now(); |
| _segmentationService->GetClassificationResult( |
| segmentation_platform::kIosModuleRankerKey, options, inputContext, |
| base::BindOnce( |
| ^(const segmentation_platform::ClassificationResult& result) { |
| weakSelf.hasReceivedMagicStackResponse = YES; |
| [weakSelf didReceiveSegmentationServiceResult:result]; |
| })); |
| } |
| |
| - (void)didReceiveSegmentationServiceResult: |
| (const segmentation_platform::ClassificationResult&)result { |
| if (result.status != segmentation_platform::PredictionStatus::kSucceeded) { |
| return; |
| } |
| |
| if ([self.homeStartDataSource isStartSurface]) { |
| base::UmaHistogramMediumTimes( |
| kMagicStackStartSegmentationRankingFetchTimeHistogram, |
| base::TimeTicks::Now() - ranking_fetch_start_time_); |
| } else { |
| base::UmaHistogramMediumTimes( |
| kMagicStackNTPSegmentationRankingFetchTimeHistogram, |
| base::TimeTicks::Now() - ranking_fetch_start_time_); |
| } |
| |
| NSMutableArray* magicStackOrder = [NSMutableArray array]; |
| for (const std::string& label : result.ordered_labels) { |
| if (label == segmentation_platform::kMostVisitedTiles) { |
| [magicStackOrder |
| addObject:@(int(ContentSuggestionsModuleType::kMostVisited))]; |
| } else if (label == segmentation_platform::kShortcuts) { |
| [magicStackOrder |
| addObject:@(int(ContentSuggestionsModuleType::kShortcuts))]; |
| } else if (label == segmentation_platform::kSafetyCheck) { |
| [magicStackOrder |
| addObject:@(int(ContentSuggestionsModuleType::kSafetyCheck))]; |
| } else if (label == segmentation_platform::kTabResumption) { |
| [magicStackOrder |
| addObject:@(int(ContentSuggestionsModuleType::kTabResumption))]; |
| } else if (label == segmentation_platform::kParcelTracking) { |
| [magicStackOrder |
| addObject:@(int(ContentSuggestionsModuleType::kParcelTracking))]; |
| } else if (label == segmentation_platform::kPriceTrackingPromo) { |
| [magicStackOrder |
| addObject:@(int(ContentSuggestionsModuleType::kPriceTrackingPromo))]; |
| } else if (label == segmentation_platform::kShopCard) { |
| [magicStackOrder |
| addObject:@(int(ContentSuggestionsModuleType::kShopCard))]; |
| } |
| } |
| |
| _magicStackOrderFromSegmentationReceived = YES; |
| _magicStackOrderFromSegmentation = magicStackOrder; |
| _latestMagicStackConfigOrder = [self latestMagicStackConfigRank]; |
| [self.delegate magicStackRankingModel:self |
| didGetLatestRankingOrder:_latestMagicStackConfigOrder]; |
| } |
| |
| - (NSArray<MagicStackModule*>*)latestMagicStackConfigRank { |
| NSMutableArray<MagicStackModule*>* magicStackOrder = [NSMutableArray array]; |
| // Always add Set Up List at the front. |
| if ([_setUpListMediator shouldShowSetUpList]) { |
| [magicStackOrder addObjectsFromArray:[_setUpListMediator setUpListConfigs]]; |
| } |
| // Currently assume ephemeral cards are always added to the front of the Magic |
| // Stack when it can show. |
| if (base::FeatureList::IsEnabled( |
| segmentation_platform::features:: |
| kSegmentationPlatformEphemeralCardRanker)) { |
| switch (_ephemeralCardToShow) { |
| case ContentSuggestionsModuleType::kPriceTrackingPromo: |
| if (_priceTrackingPromoMediator && |
| _priceTrackingPromoMediator.priceTrackingPromoItemToShow) { |
| [magicStackOrder addObject:_priceTrackingPromoMediator |
| .priceTrackingPromoItemToShow]; |
| } |
| break; |
| case ContentSuggestionsModuleType::kSendTabPromo: |
| if (send_tab_to_self:: |
| IsSendTabIOSPushNotificationsEnabledWithMagicStackCard() && |
| _sendTabPromoMediator && |
| _sendTabPromoMediator.sendTabPromoItemToShow) { |
| [magicStackOrder |
| addObject:_sendTabPromoMediator.sendTabPromoItemToShow]; |
| } |
| break; |
| case ContentSuggestionsModuleType::kTips: |
| case ContentSuggestionsModuleType::kTipsWithProductImage: { |
| if (IsTipsMagicStackEnabled() && _tipsMediator && _tipsMediator.state) { |
| [magicStackOrder addObject:_tipsMediator.state]; |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| for (NSNumber* moduleNumber in _magicStackOrderFromSegmentation) { |
| ContentSuggestionsModuleType moduleType = |
| (ContentSuggestionsModuleType)[moduleNumber intValue]; |
| switch (moduleType) { |
| case ContentSuggestionsModuleType::kMostVisited: { |
| BOOL shouldShowMostVisitedTileInMagicStack = |
| _mostVisitedTilesMediator.mostVisitedConfig.inMagicStack; |
| BOOL isMostVisitedTileVisible = _prefService->GetBoolean( |
| prefs::kHomeCustomizationMostVisitedEnabled); |
| BOOL hasMostVisitedItems = [_mostVisitedTilesMediator.mostVisitedConfig |
| .mostVisitedItems count] > 0; |
| if (shouldShowMostVisitedTileInMagicStack && isMostVisitedTileVisible && |
| hasMostVisitedItems) { |
| [magicStackOrder |
| addObject:_mostVisitedTilesMediator.mostVisitedConfig]; |
| } |
| break; |
| } |
| case ContentSuggestionsModuleType::kTabResumption: |
| if (![self shouldShowTabResumption]) { |
| break; |
| } |
| // If ShouldHideIrrelevantModules() is enabled and it is not ranked as |
| // the first two modules, do not add it to the Magic Stack. |
| if (ShouldHideIrrelevantModules() && [magicStackOrder count] > 1) { |
| break; |
| } |
| [magicStackOrder addObject:_tabResumptionMediator.itemConfig]; |
| break; |
| case ContentSuggestionsModuleType::kSafetyCheck: { |
| // Handles adding Safety Check to Magic Stack. Disables/hides if: |
| // - Manually disabled or disabled via preferences. |
| // - No current or previous issues, to avoid consistently displaying the |
| // "All Safe" state and taking up carousel space for other modules. |
| // - Irrelevant modules are hidden and it's not the first ranked module. |
| BOOL disabled = |
| !IsSafetyCheckMagicStackEnabled() || |
| safety_check_prefs::IsSafetyCheckInMagicStackDisabled( |
| IsHomeCustomizationEnabled() ? _prefService : _localState); |
| |
| if (disabled) { |
| base::UmaHistogramEnumeration( |
| kIOSSafetyCheckMagicStackHiddenReason, |
| IOSSafetyCheckHiddenReason::kManuallyDisabled); |
| break; |
| } |
| |
| int previousIssuesCount = _localState->GetInteger( |
| prefs::kHomeCustomizationMagicStackSafetyCheckIssuesCount); |
| |
| int issuesCount = |
| [_safetyCheckMediator.safetyCheckState numberOfIssues]; |
| |
| BOOL hidden = ShouldHideSafetyCheckModuleIfNoIssues() && |
| (previousIssuesCount == 0) && |
| (previousIssuesCount == issuesCount); |
| |
| if (hidden) { |
| base::UmaHistogramEnumeration(kIOSSafetyCheckMagicStackHiddenReason, |
| IOSSafetyCheckHiddenReason::kNoIssues); |
| break; |
| } |
| |
| // If ShouldHideIrrelevantModules() is enabled and it is not the first |
| // ranked module, do not add it to the Magic Stack. |
| if (!ShouldHideIrrelevantModules() || [magicStackOrder count] == 0) { |
| [magicStackOrder addObject:_safetyCheckMediator.safetyCheckState]; |
| } |
| |
| break; |
| } |
| case ContentSuggestionsModuleType::kShortcuts: |
| [magicStackOrder addObject:_shortcutsMediator.shortcutsConfig]; |
| break; |
| case ContentSuggestionsModuleType::kShopCard: |
| if (_shopCardMediator && _shopCardMediator.shopCardItemToShow) { |
| [magicStackOrder addObject:_shopCardMediator.shopCardItemToShow]; |
| } |
| break; |
| case ContentSuggestionsModuleType::kParcelTracking: |
| if (IsIOSParcelTrackingEnabled() && |
| !IsParcelTrackingDisabled( |
| IsHomeCustomizationEnabled() ? _prefService : _localState) && |
| _parcelTrackingMediator.parcelTrackingItemToShow) { |
| [magicStackOrder |
| addObject:_parcelTrackingMediator.parcelTrackingItemToShow]; |
| } |
| break; |
| default: |
| // These module types should not have been added by the logic |
| // receiving the order list from Segmentation. |
| NOTREACHED(); |
| } |
| } |
| return magicStackOrder; |
| } |
| |
| // Returns NO if client is expecting the order from Segmentation and it has not |
| // returned yet. |
| - (BOOL)isMagicStackOrderReady { |
| return _magicStackOrderFromSegmentationReceived; |
| } |
| |
| // Shows the tab resumption tile with the given `item` configuration. |
| - (void)showTabResumptionWithItem:(TabResumptionItem*)item { |
| if (tab_resumption_prefs::IsLastOpenedURL(item.tabURL, _prefService)) { |
| return; |
| } |
| |
| if (![self isMagicStackOrderReady]) { |
| return; |
| } |
| NSArray<MagicStackModule*>* rank = [self latestMagicStackConfigRank]; |
| NSUInteger index = [rank indexOfObject:item]; |
| [self.delegate magicStackRankingModel:self didInsertItem:item atIndex:index]; |
| } |
| |
| // Returns YES if the tab resumption module should added into the Magic Stack. |
| - (BOOL)shouldShowTabResumption { |
| return IsTabResumptionEnabled() && |
| !tab_resumption_prefs::IsTabResumptionDisabled( |
| IsHomeCustomizationEnabled() ? _prefService : _localState) && |
| _tabResumptionMediator.itemConfig; |
| } |
| |
| // Returns `YES` if Lens is enabled. |
| - (BOOL)isLensEnabled { |
| bool isGoogleDefaultSearchProvider = |
| search::DefaultSearchProviderIsGoogle(_templateURLService); |
| |
| return lens_availability::CheckAndLogAvailabilityForLensEntryPoint( |
| LensEntrypoint::NewTabPage, isGoogleDefaultSearchProvider); |
| } |
| |
| @end |