// 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/ui/omnibox/popup/popup_debug_info_view_controller.h"
#import "base/apple/foundation_util.h"
#import "components/omnibox/browser/autocomplete_match_type.h"
#import "components/omnibox/browser/autocomplete_provider.h"
#import "components/variations/variations_switches.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/ui/omnibox/popup/autocomplete_match_formatter.h"
#import "ios/chrome/browser/ui/omnibox/popup/debugger/omnibox_autocomplete_event.h"
#import "ios/chrome/browser/ui/omnibox/popup/debugger/omnibox_autocomplete_event_view_controller.h"
#import "ios/chrome/browser/ui/omnibox/popup/debugger/omnibox_event.h"
#import "ios/chrome/browser/ui/omnibox/popup/debugger/omnibox_remote_suggestion_event.h"
#import "ios/chrome/browser/ui/omnibox/popup/debugger/omnibox_remote_suggestion_event_view_controller.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
namespace {
typedef NS_ENUM(NSUInteger, SectionRows) {
SuggestionDetailsRow = 0,
RelevanceRow,
GroupIdRow,
IsTabMatchRow,
SupportsDeletionRow,
ProviderRow,
SuggestionTypeRow,
SectionRowsCount
};
/// Debug text view used to display text that can be selected.
UITextView* DebugTextView() {
UITextView* textView = [[UITextView alloc] init];
textView.translatesAutoresizingMaskIntoConstraints = NO;
textView.editable = NO;
textView.scrollEnabled = NO;
textView.font = [UIFont systemFontOfSize:15];
return textView;
}
/// Text field used to input variation id.
UITextField* ForceVariationTextField() {
UITextField* textField = [[UITextField alloc] init];
textField.translatesAutoresizingMaskIntoConstraints = NO;
textField.borderStyle = UITextBorderStyleBezel;
textField.backgroundColor = UIColor.lightGrayColor;
textField.keyboardType = UIKeyboardTypeNumberPad;
textField.placeholder = @"Force variation ID";
return textField;
}
/// Stack view containing variation id information.
UIStackView* VariationStackView() {
UIStackView* stackView = [[UIStackView alloc] init];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.axis = UILayoutConstraintAxisVertical;
stackView.distribution = UIStackViewDistributionFill;
stackView.spacing = 10;
return stackView;
}
/// Button to open app settings.
UIButton* SettingsButton() {
UIAction* openSettings = [UIAction actionWithHandler:^(UIAction* action) {
NSURL* url =
[[NSURL alloc] initWithString:UIApplicationOpenSettingsURLString];
[[UIApplication sharedApplication] openURL:url
options:@{}
completionHandler:nil];
}];
UIButtonConfiguration* configuration =
[UIButtonConfiguration grayButtonConfiguration];
configuration.title = @"Open iOS Settings";
UIImageSymbolConfiguration* config = [UIImageSymbolConfiguration
configurationWithWeight:UIImageSymbolWeightLight];
configuration.image = DefaultSymbolWithConfiguration(@"gear.circle", config);
UIButton* button = [UIButton buttonWithConfiguration:configuration
primaryAction:openSettings];
button.translatesAutoresizingMaskIntoConstraints = NO;
return button;
}
/// Label showing instrunction to force variation Id.
UILabel* VariationInstructionLabel() {
UILabel* label = [[UILabel alloc] init];
label.translatesAutoresizingMaskIntoConstraints = NO;
label.numberOfLines = 0;
label.text = @"Copy these in iOS Settings > Experimental Settings > EXTRA "
@"FLAGS (ONE PER LINE)";
return label;
}
UITableView* SuggestionsTableView() {
UITableView* tableView = [[UITableView alloc] initWithFrame:CGRectZero];
[tableView registerClass:[UITableViewCell class]
forCellReuseIdentifier:@"Cell"];
[tableView registerClass:[UITableViewHeaderFooterView class]
forHeaderFooterViewReuseIdentifier:NSStringFromClass(
[UITableViewHeaderFooterView
class])];
tableView.translatesAutoresizingMaskIntoConstraints = NO;
return tableView;
}
// The initial four characters in the response body are redundant.
const NSInteger kRemoteSuggestionServiceResponseBodyJsonStartingIndex = 4;
} // namespace
@interface PopupDebugInfoViewController () <UITextFieldDelegate,
UITableViewDelegate,
UITableViewDataSource>
@property(nonatomic, strong) UITextView* activeVariationIDTextView;
@property(nonatomic, strong) UITextField* variationIDTextField;
@property(nonatomic, strong) UITextView* enableVariationIDTextView;
@property(nonatomic, strong) UITextView* disableVariationIDsTextView;
@property(nonatomic, strong) UITableView* tableView;
@property(nonatomic, strong) UILabel* variationInstructionLabel;
@property(nonatomic, strong) UIButton* settingsButton;
@property(nonatomic, strong) NSArray<NSNumber*>* activeVariationIDs;
@property(nonatomic, strong) UIStackView* variationStackView;
@end
@implementation PopupDebugInfoViewController {
// In reverse chronological order: index 0 is most recent.
NSMutableArray<id<OmniboxEvent>>* _events;
}
- (instancetype)init {
if ((self = [super initWithNibName:nil bundle:nil])) {
_activeVariationIDs = @[];
_variationStackView = VariationStackView();
_activeVariationIDTextView = DebugTextView();
_variationIDTextField = ForceVariationTextField();
_variationIDTextField.delegate = self;
_settingsButton = SettingsButton();
_variationInstructionLabel = VariationInstructionLabel();
_enableVariationIDTextView = DebugTextView();
_disableVariationIDsTextView = DebugTextView();
_tableView = SuggestionsTableView();
[_tableView setDelegate:self];
[_tableView setDataSource:self];
_events = [[NSMutableArray alloc] init];
[_variationIDTextField addTarget:self
action:@selector(textFieldDidChange:)
forControlEvents:UIControlEventEditingChanged];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
UIScrollView* scrollView = [[UIScrollView alloc] init];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.backgroundColor = UIColor.systemBackgroundColor;
[self.view addSubview:scrollView];
AddSameConstraints(self.view, scrollView);
UIStackView* stackView = self.variationStackView;
[scrollView addSubview:stackView];
AddSameConstraints(stackView, scrollView);
[NSLayoutConstraint activateConstraints:@[
[scrollView.widthAnchor constraintEqualToAnchor:stackView.widthAnchor],
[scrollView.heightAnchor constraintEqualToAnchor:self.view.heightAnchor],
[stackView.heightAnchor constraintEqualToAnchor:self.view.heightAnchor],
]];
[stackView addArrangedSubview:self.activeVariationIDTextView];
[stackView addArrangedSubview:self.variationIDTextField];
[stackView addArrangedSubview:self.settingsButton];
[stackView addArrangedSubview:self.variationInstructionLabel];
[stackView addArrangedSubview:self.enableVariationIDTextView];
[stackView addArrangedSubview:self.disableVariationIDsTextView];
[stackView addArrangedSubview:_tableView];
[NSLayoutConstraint activateConstraints:@[
[_tableView.widthAnchor constraintEqualToAnchor:stackView.widthAnchor],
[_tableView.topAnchor
constraintEqualToAnchor:self.disableVariationIDsTextView.bottomAnchor
constant:16],
[_tableView.bottomAnchor constraintEqualToAnchor:stackView.bottomAnchor
constant:-16]
]];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(doneButtonPressed)];
}
#pragma mark - UITextFieldDelegate
- (BOOL)textField:(UITextField*)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString*)string {
if (textField == self.variationIDTextField) {
if (!string.length) {
return YES;
}
NSCharacterSet* nonDecimalDigit =
[NSCharacterSet decimalDigitCharacterSet].invertedSet;
if ([string rangeOfCharacterFromSet:nonDecimalDigit].location ==
NSNotFound) {
return YES;
}
// Allow pasting variation ids with a leading t.
if ([string characterAtIndex:0] == 't') {
NSString* substring = [string substringFromIndex:1];
if ([substring intValue]) {
textField.text = substring;
}
}
return NO;
}
return YES;
}
- (void)textFieldDidChange:(id)sender {
[self updateForceVariationTextViews];
}
#pragma mark - PopupDebugInfoConsumer
- (void)setVariationIDString:(NSString*)string {
NSCharacterSet* whitespaceSet =
[NSCharacterSet whitespaceAndNewlineCharacterSet];
NSString* trimmedString =
[string stringByTrimmingCharactersInSet:whitespaceSet];
self.activeVariationIDTextView.text =
[NSString stringWithFormat:@"Active variation IDs: %@", trimmedString];
NSArray<NSString*>* stringIds =
[trimmedString componentsSeparatedByString:@" "];
self.activeVariationIDs = [[stringIds valueForKey:@"intValue"]
filteredArrayUsingPredicate:[NSPredicate
predicateWithFormat:@"SELF != 0"]];
[self updateForceVariationTextViews];
}
#pragma mark - AutocompleteControllerObserver
- (void)autocompleteController:(AutocompleteController*)controller
didStartWithInput:(const AutocompleteInput&)input {
[_events removeAllObjects];
[_tableView reloadData];
}
- (void)autocompleteController:(AutocompleteController*)controller
didUpdateResultChangingDefaultMatch:(BOOL)defaultMatchChanged {
OmniboxAutocompleteEvent* event = [[OmniboxAutocompleteEvent alloc]
initWithAutocompleteController:controller];
[_events insertObject:event atIndex:0];
[_tableView reloadData];
}
#pragma mark - RemoteSuggestionsServiceObserver
- (void)remoteSuggestionsService:(RemoteSuggestionsService*)service
createdRequestWithIdentifier:
(const base::UnguessableToken&)requestIdentifier
request:(const network::ResourceRequest*)request {
OmniboxRemoteSuggestionEvent* event = [[OmniboxRemoteSuggestionEvent alloc]
initWithUniqueIdentifier:requestIdentifier];
[_events insertObject:event atIndex:0];
[_tableView reloadData];
}
- (void)remoteSuggestionsService:(RemoteSuggestionsService*)service
startedRequestWithIdentifier:
(const base::UnguessableToken&)requestIdentifier
requestBody:(NSString*)requestBody
URLLoader:(network::SimpleURLLoader*)URLLoader {
NSUInteger indexOfFoundEventElement =
[_events indexOfObjectPassingTest:^BOOL(id<OmniboxEvent> event,
NSUInteger, BOOL*) {
return event.type == kRemoteSuggestionUpdate &&
base::apple::ObjCCastStrict<OmniboxRemoteSuggestionEvent>(event)
.uniqueIdentifier == requestIdentifier;
}];
if (indexOfFoundEventElement != NSNotFound) {
base::apple::ObjCCastStrict<OmniboxRemoteSuggestionEvent>(
_events[indexOfFoundEventElement])
.requestBody = requestBody;
NSIndexPath* indexPath =
[NSIndexPath indexPathForRow:indexOfFoundEventElement inSection:0];
[_tableView reloadRowsAtIndexPaths:@[ indexPath ]
withRowAnimation:UITableViewRowAnimationNone];
}
}
- (void)remoteSuggestionsService:(RemoteSuggestionsService*)service
completedRequestWithIdentifier:
(const base::UnguessableToken&)requestIdentifier
responseCode:(NSInteger)code
responseBody:(NSString*)responseBody {
NSUInteger indexOfFoundEventElement =
[_events indexOfObjectPassingTest:^BOOL(id<OmniboxEvent> event,
NSUInteger, BOOL*) {
return event.type == kRemoteSuggestionUpdate &&
base::apple::ObjCCastStrict<OmniboxRemoteSuggestionEvent>(event)
.uniqueIdentifier == requestIdentifier;
}];
if (indexOfFoundEventElement != NSNotFound) {
OmniboxRemoteSuggestionEvent* event =
base::apple::ObjCCastStrict<OmniboxRemoteSuggestionEvent>(
_events[indexOfFoundEventElement]);
event.responseBody = [responseBody
substringFromIndex:
kRemoteSuggestionServiceResponseBodyJsonStartingIndex];
event.responseCode = code;
NSIndexPath* indexPath =
[NSIndexPath indexPathForRow:indexOfFoundEventElement inSection:0];
[_tableView reloadRowsAtIndexPaths:@[ indexPath ]
withRowAnimation:UITableViewRowAnimationNone];
}
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section {
return _events.count;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView {
return 1;
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
UITableViewCell* cell =
[tableView dequeueReusableCellWithIdentifier:@"Cell"
forIndexPath:indexPath];
UIListContentConfiguration* content = cell.defaultContentConfiguration;
id<OmniboxEvent> event = _events[indexPath.row];
content.text = event.title;
cell.contentConfiguration = content;
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView*)tableView
didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
id<OmniboxEvent> event = _events[indexPath.row];
if (event.type == kAutocompleteUpdate) {
OmniboxAutocompleteEventViewController* vc =
[[OmniboxAutocompleteEventViewController alloc] init];
vc.event = (OmniboxAutocompleteEvent*)event;
[self.navigationController pushViewController:vc animated:YES];
} else {
OmniboxRemoteSuggestionEventViewController* vc =
[[OmniboxRemoteSuggestionEventViewController alloc] init];
vc.event = (OmniboxRemoteSuggestionEvent*)event;
[self.navigationController pushViewController:vc animated:YES];
}
}
#pragma mark - private
- (void)doneButtonPressed {
[self.presentingViewController dismissViewControllerAnimated:YES
completion:nil];
}
- (void)updateForceVariationTextViews {
NSInteger forcedId = [self.variationIDTextField.text intValue];
NSString* enabledVariationString = @"";
NSString* disabledVariationString = @"";
if (forcedId > 0) {
enabledVariationString = [NSString
stringWithFormat:@"%s=t%ld", variations::switches::kForceVariationIds,
forcedId];
NSArray* disabledIds = [self.activeVariationIDs
filteredArrayUsingPredicate:
[NSPredicate
predicateWithFormat:@"SELF != %@",
[NSNumber numberWithInteger:forcedId]]];
if (disabledIds.count) {
disabledVariationString = [NSString
stringWithFormat:@"%s=t%@",
variations::switches::kForceDisableVariationIds,
[disabledIds componentsJoinedByString:@",t"]];
}
}
self.enableVariationIDTextView.text = enabledVariationString;
self.disableVariationIDsTextView.text = disabledVariationString;
self.enableVariationIDTextView.hidden =
!self.enableVariationIDTextView.text.length;
self.disableVariationIDsTextView.hidden =
!self.disableVariationIDsTextView.text.length;
self.variationInstructionLabel.hidden =
self.enableVariationIDTextView.hidden &&
self.disableVariationIDsTextView.hidden;
self.settingsButton.hidden = self.variationInstructionLabel.hidden;
}
@end