Commit 3457ee86 authored by Sebastien Lalancette's avatar Sebastien Lalancette Committed by Commit Bot

Reuse ConfirmationAlertViewController for QRGeneratorViewController

Also, allow usage of fixed size image to prevent scaling-up the QR code
(it becomes too big to be scanned effectively).

Sample:
https://screenshot.googleplex.com/xYWmsrTzZpD.png

Bug: 1064990
Change-Id: Ibd1e7f5f1149e436929ca8c33b1dae7807172411
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2153391
Commit-Queue: Sebastien Lalancette <seblalancette@chromium.org>
Reviewed-by: default avatarJavier Ernesto Flores Robles <javierrobles@chromium.org>
Reviewed-by: default avatarMark Cogan <marq@chromium.org>
Cr-Commit-Position: refs/heads/master@{#761167}
parent e513cef8
...@@ -1609,6 +1609,9 @@ Handoff must also be enabled in the General section of Settings, and your device ...@@ -1609,6 +1609,9 @@ Handoff must also be enabled in the General section of Settings, and your device
<message name="IDS_IOS_TOGGLE_SETTING_SWITCH_ACCESSIBILITY_HINT" desc="Action hint for any switch in settings. This is spoken by VoiceOver. [iOS only]"> <message name="IDS_IOS_TOGGLE_SETTING_SWITCH_ACCESSIBILITY_HINT" desc="Action hint for any switch in settings. This is spoken by VoiceOver. [iOS only]">
Double tap to toggle setting Double tap to toggle setting
</message> </message>
<message name="IDS_IOS_SHARE_BUTTON_LABEL" desc="Label of a button which will initiate a share flow. [iOS only]">
Share...
</message>
<message name="IDS_IOS_SHARE_EMAIL_COMPLETE" desc="Message shown when email share has been sent. [Length: Unknown. Keep it short.] [iOS only]"> <message name="IDS_IOS_SHARE_EMAIL_COMPLETE" desc="Message shown when email share has been sent. [Length: Unknown. Keep it short.] [iOS only]">
Mail sent. Mail sent.
</message> </message>
......
...@@ -12,9 +12,10 @@ source_set("ui") { ...@@ -12,9 +12,10 @@ source_set("ui") {
] ]
deps = [ deps = [
"//base", "//base",
"//ios/chrome/app/strings",
"//ios/chrome/browser/ui/commands", "//ios/chrome/browser/ui/commands",
"//ios/chrome/browser/ui/util", "//ios/chrome/browser/ui/util",
"//ios/chrome/common/ui/colors", "//ios/chrome/common/ui/confirmation_alert",
"//ui/base", "//ui/base",
"//url:url", "//url:url",
] ]
...@@ -36,6 +37,7 @@ source_set("qr_generator") { ...@@ -36,6 +37,7 @@ source_set("qr_generator") {
"//ios/chrome/browser/main:public", "//ios/chrome/browser/main:public",
"//ios/chrome/browser/ui/commands", "//ios/chrome/browser/ui/commands",
"//ios/chrome/browser/ui/coordinators:chrome_coordinators", "//ios/chrome/browser/ui/coordinators:chrome_coordinators",
"//ios/chrome/common/ui/confirmation_alert",
"//net", "//net",
] ]
} }
...@@ -44,15 +46,18 @@ source_set("unit_tests") { ...@@ -44,15 +46,18 @@ source_set("unit_tests") {
configs += [ "//build/config/compiler:enable_arc" ] configs += [ "//build/config/compiler:enable_arc" ]
testonly = true testonly = true
sources = [ sources = [
"qr_generator_coordinator_unittest.mm",
"qr_generator_util_unittest.mm", "qr_generator_util_unittest.mm",
"qr_generator_view_controller_unittest.mm",
] ]
deps = [ deps = [
":qr_generator",
":ui", ":ui",
"//base", "//base",
"//base/test:test_support", "//base/test:test_support",
"//ios/chrome/browser/main:test_support",
"//ios/chrome/browser/ui/commands", "//ios/chrome/browser/ui/commands",
"//ios/chrome/common/ui/colors", "//ios/chrome/common/ui/confirmation_alert",
"//ios/chrome/test:test_support",
"//ios/chrome/test/fakes", "//ios/chrome/test/fakes",
"//ios/web", "//ios/web",
"//ios/web/public/test", "//ios/web/public/test",
......
...@@ -9,27 +9,23 @@ ...@@ -9,27 +9,23 @@
#import "ios/chrome/browser/ui/commands/command_dispatcher.h" #import "ios/chrome/browser/ui/commands/command_dispatcher.h"
#import "ios/chrome/browser/ui/commands/qr_generation_commands.h" #import "ios/chrome/browser/ui/commands/qr_generation_commands.h"
#import "ios/chrome/browser/ui/qr_generator/qr_generator_view_controller.h" #import "ios/chrome/browser/ui/qr_generator/qr_generator_view_controller.h"
#import "ios/chrome/common/ui/confirmation_alert/confirmation_alert_action_handler.h"
#import "net/base/mac/url_conversions.h" #import "net/base/mac/url_conversions.h"
#if !defined(__has_feature) || !__has_feature(objc_arc) #if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
@interface QRGeneratorCoordinator () { @interface QRGeneratorCoordinator () <ConfirmationAlertActionHandler> {
// URL of a page to generate a QR code for. // URL of a page to generate a QR code for.
GURL _URL; GURL _URL;
} }
// To be used to dispatch commands to the browser. // To be used to handle behaviors that go outside the scope of this class.
@property(nonatomic, strong) CommandDispatcher* dispatcher; @property(nonatomic, strong) id<QRGenerationCommands> handler;
// Main view controller which will be the parent, or ancestor, of other
// view controllers related to this feature.
@property(nonatomic, strong) UINavigationController* viewController;
// View controller used to display the QR code and actions. // View controller used to display the QR code and actions.
@property(nonatomic, strong) @property(nonatomic, strong) QRGeneratorViewController* viewController;
QRGeneratorViewController* qrGeneratorViewController;
// Title of a page to generate a QR code for. // Title of a page to generate a QR code for.
@property(nonatomic, copy) NSString* title; @property(nonatomic, copy) NSString* title;
...@@ -53,17 +49,15 @@ ...@@ -53,17 +49,15 @@
#pragma mark - Chrome Coordinator #pragma mark - Chrome Coordinator
- (void)start { - (void)start {
self.dispatcher = self.browser->GetCommandDispatcher(); self.handler = HandlerForProtocol(self.browser->GetCommandDispatcher(),
QRGenerationCommands);
self.qrGeneratorViewController = [[QRGeneratorViewController alloc] init];
self.qrGeneratorViewController.handler =
HandlerForProtocol(self.dispatcher, QRGenerationCommands);
[self.qrGeneratorViewController setPageURL:net::NSURLWithGURL(_URL)]; self.viewController = [[QRGeneratorViewController alloc] init];
self.viewController = [[UINavigationController alloc]
initWithRootViewController:self.qrGeneratorViewController];
[self.viewController setModalPresentationStyle:UIModalPresentationFormSheet]; [self.viewController setModalPresentationStyle:UIModalPresentationFormSheet];
[self.viewController setPageURL:net::NSURLWithGURL(_URL)];
[self.viewController setTitleString:self.title];
[self.viewController setActionHandler:self];
[self.baseViewController presentViewController:self.viewController [self.baseViewController presentViewController:self.viewController
animated:YES animated:YES
...@@ -72,14 +66,26 @@ ...@@ -72,14 +66,26 @@
} }
- (void)stop { - (void)stop {
[self.viewController.presentingViewController [self.viewController dismissViewControllerAnimated:YES completion:nil];
dismissViewControllerAnimated:YES
completion:nil];
self.qrGeneratorViewController = nil;
self.viewController = nil; self.viewController = nil;
[super stop]; [super stop];
} }
#pragma mark - ConfirmationAlertActionHandler
- (void)confirmationAlertDone {
[self.handler hideQRCode];
}
- (void)confirmationAlertPrimaryAction {
// No-op.
// TODO crbug.com/1064990: Add sharing action.
}
- (void)confirmationAlertLearnMoreAction {
// No-op.
// TODO crbug.com/1064990: Add learn more behavior.
}
@end @end
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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/qr_generator/qr_generator_coordinator.h"
#import "base/mac/foundation_util.h"
#import "base/test/task_environment.h"
#import "ios/chrome/browser/main/test_browser.h"
#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
#import "ios/chrome/browser/ui/commands/qr_generation_commands.h"
#import "ios/chrome/browser/ui/qr_generator/qr_generator_view_controller.h"
#import "ios/chrome/common/ui/confirmation_alert/confirmation_alert_action_handler.h"
#import "ios/chrome/test/scoped_key_window.h"
#include "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
class QRGeneratorCoordinatorTest : public PlatformTest {
protected:
QRGeneratorCoordinatorTest() : browser_(std::make_unique<TestBrowser>()) {
base_view_controller_ = [[UIViewController alloc] init];
[scoped_key_window_.Get() setRootViewController:base_view_controller_];
}
void SetUp() override {
mock_qr_generation_commands_handler_ =
OCMStrictProtocolMock(@protocol(QRGenerationCommands));
[browser_->GetCommandDispatcher()
startDispatchingToTarget:mock_qr_generation_commands_handler_
forProtocol:@protocol(QRGenerationCommands)];
}
base::test::TaskEnvironment task_environment_;
id mock_qr_generation_commands_handler_;
std::unique_ptr<TestBrowser> browser_;
ScopedKeyWindow scoped_key_window_;
UIViewController* base_view_controller_;
};
// Tests that a Done button gets added to the navigation bar, and its action
// dispatches the right command.
TEST_F(QRGeneratorCoordinatorTest, Done_DispatchesCommand) {
// Set-up mocked handler.
[[mock_qr_generation_commands_handler_ expect] hideQRCode];
// Create and start coordinator.
QRGeneratorCoordinator* coordinator = [[QRGeneratorCoordinator alloc]
initWithBaseViewController:base_view_controller_
browser:browser_.get()
title:@"Does not matter"
URL:GURL()];
ASSERT_EQ(base_view_controller_, coordinator.baseViewController);
ASSERT_FALSE(base_view_controller_.presentedViewController);
[coordinator start];
ASSERT_TRUE(base_view_controller_.presentedViewController);
ASSERT_TRUE([base_view_controller_.presentedViewController
isKindOfClass:[QRGeneratorViewController class]]);
QRGeneratorViewController* viewController =
base::mac::ObjCCastStrict<QRGeneratorViewController>(
base_view_controller_.presentedViewController);
// Mimick click on done button.
[viewController.actionHandler confirmationAlertDone];
// Callback should've gotten invoked.
[mock_qr_generation_commands_handler_ verify];
}
...@@ -6,14 +6,12 @@ ...@@ -6,14 +6,12 @@
#define IOS_CHROME_BROWSER_UI_QR_GENERATOR_QR_GENERATOR_VIEW_CONTROLLER_H_ #define IOS_CHROME_BROWSER_UI_QR_GENERATOR_QR_GENERATOR_VIEW_CONTROLLER_H_
#import "ios/chrome/browser/ui/commands/qr_generation_commands.h" #import "ios/chrome/browser/ui/commands/qr_generation_commands.h"
#import "ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.h"
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
// View controller that displays a QR code representing a given website. // View controller that displays a QR code representing a given website.
@interface QRGeneratorViewController : UIViewController @interface QRGeneratorViewController : ConfirmationAlertViewController
// Command handler for communicating with other components of the app.
@property(nonatomic, weak) id<QRGenerationCommands> handler;
// URL of the page to generate a QR code for. // URL of the page to generate a QR code for.
@property(nonatomic, copy) NSURL* pageURL; @property(nonatomic, copy) NSURL* pageURL;
......
...@@ -6,121 +6,43 @@ ...@@ -6,121 +6,43 @@
#import "ios/chrome/browser/ui/qr_generator/qr_generator_view_controller.h" #import "ios/chrome/browser/ui/qr_generator/qr_generator_view_controller.h"
#import "ios/chrome/browser/ui/qr_generator/qr_generator_util.h" #import "ios/chrome/browser/ui/qr_generator/qr_generator_util.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h" #include "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/common/ui/colors/UIColor+cr_semantic_colors.h" #include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc) #if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
NSString* const kQRGeneratorDoneButtonId = @"kQRGeneratorDoneButtonId";
// Height and width of the QR code image, in points. // Height and width of the QR code image, in points.
const CGFloat kQRCodeImageSize = 200.0; const CGFloat kQRCodeImageSize = 200.0;
@interface QRGeneratorViewController () @interface QRGeneratorViewController ()
// View for the QR code image.
@property(nonatomic, strong) UIImageView* qrCodeView;
// Orientation-specific constraints.
@property(nonatomic, strong)
NSArray<NSLayoutConstraint*>* compactHeightConstraints;
@property(nonatomic, strong)
NSArray<NSLayoutConstraint*>* regularHeightConstraints;
@end @end
@implementation QRGeneratorViewController @implementation QRGeneratorViewController
#pragma mark - UIViewController #pragma mark - UIViewController
- (void)viewDidLoad { - (void)loadView {
[super viewDidLoad]; self.image = [self createQRCodeImage];
self.imageHasFixedSize = YES;
[self setUpNavigation];
self.qrCodeView = [self createQRCodeImageView];
[self.view addSubview:self.qrCodeView];
[self setupConstraints];
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { self.subtitleString = [self.pageURL host];
[super traitCollectionDidChange:previousTraitCollection];
if (previousTraitCollection.verticalSizeClass != self.primaryActionAvailable = YES;
self.traitCollection.verticalSizeClass) { self.primaryActionString = l10n_util::GetNSString(IDS_IOS_SHARE_BUTTON_LABEL);
[self updateViewConstraints];
}
}
- (void)updateViewConstraints { self.helpButtonAvailable = YES;
// Check if we're in landscape mode or not. [super loadView];
if (IsCompactHeight(self)) {
[NSLayoutConstraint deactivateConstraints:self.regularHeightConstraints];
[NSLayoutConstraint activateConstraints:self.compactHeightConstraints];
} else {
[NSLayoutConstraint deactivateConstraints:self.compactHeightConstraints];
[NSLayoutConstraint activateConstraints:self.regularHeightConstraints];
}
[super updateViewConstraints];
} }
#pragma mark - Private Methods #pragma mark - Private Methods
- (void)setUpNavigation { - (UIImage*)createQRCodeImage {
// Set shadowImage to an empty image to remove the separator between the
// navigation bar and the main content.
self.navigationController.navigationBar.shadowImage = [[UIImage alloc] init];
// Set background color.
[self.navigationController.navigationBar setTranslucent:NO];
[self.navigationController.navigationBar
setBarTintColor:UIColor.cr_systemBackgroundColor];
[self.navigationController.view
setBackgroundColor:UIColor.cr_systemBackgroundColor];
// Add a Done button to the navigation bar.
UIBarButtonItem* doneButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self.handler
action:@selector(hideQRCode)];
doneButton.accessibilityIdentifier = kQRGeneratorDoneButtonId;
self.navigationItem.rightBarButtonItem = doneButton;
}
- (UIImageView*)createQRCodeImageView {
NSData* urlData = NSData* urlData =
[[self.pageURL absoluteString] dataUsingEncoding:NSUTF8StringEncoding]; [[self.pageURL absoluteString] dataUsingEncoding:NSUTF8StringEncoding];
UIImage* qrCodeImage = GenerateQRCode(urlData, kQRCodeImageSize); return GenerateQRCode(urlData, kQRCodeImageSize);
UIImageView* qrCodeView = [[UIImageView alloc] initWithImage:qrCodeImage];
qrCodeView.translatesAutoresizingMaskIntoConstraints = NO;
return qrCodeView;
}
- (void)setupConstraints {
// Set-up orientation-specific constraints.
self.compactHeightConstraints = @[
[self.qrCodeView.topAnchor constraintEqualToAnchor:self.view.topAnchor
constant:20.0],
];
self.regularHeightConstraints = @[
[self.qrCodeView.topAnchor constraintEqualToAnchor:self.view.topAnchor
constant:60.0],
];
// Activate the right orientation-specific constraint.
[self updateViewConstraints];
// Activate common constraints.
NSArray* commonConstraints = @[
[self.qrCodeView.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
];
[NSLayoutConstraint activateConstraints:commonConstraints];
} }
@end @end
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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/qr_generator/qr_generator_view_controller.h"
#import "ios/chrome/browser/ui/commands/qr_generation_commands.h"
#import "ios/chrome/common/ui/colors/UIColor+cr_semantic_colors.h"
#include "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
class QRGeneratorViewControllerTest : public PlatformTest {
public:
QRGeneratorViewControllerTest()
: view_controller_([[QRGeneratorViewController alloc] init]) {}
protected:
void SetUp() override { [view_controller_ loadViewIfNeeded]; }
QRGeneratorViewController* view_controller_;
private:
DISALLOW_COPY_AND_ASSIGN(QRGeneratorViewControllerTest);
};
// Tests that a Done button gets added to the navigation bar, and its action
// dispatches the right command.
TEST_F(QRGeneratorViewControllerTest, DoneButton_DispatchesCommand) {
// Set-up mocked handler.
id mockedHandler = OCMStrictProtocolMock(@protocol(QRGenerationCommands));
[[mockedHandler expect] hideQRCode];
[view_controller_ setHandler:mockedHandler];
// Set-up mocked navigation item.
__block UIBarButtonItem* capturedButton;
id navItemMock = OCMStrictClassMock([UINavigationItem class]);
[[navItemMock expect]
setRightBarButtonItem:[OCMArg
checkWithBlock:^BOOL(UIBarButtonItem* button) {
capturedButton = button;
return !!capturedButton;
}]];
id vcPartialMock = OCMPartialMock(view_controller_);
OCMStub([vcPartialMock navigationItem]).andReturn(navItemMock);
[view_controller_ viewDidLoad];
// Button should've been given to the navigationItem.
[navItemMock verify];
// Simulate a click on the button.
[[UIApplication sharedApplication] sendAction:capturedButton.action
to:capturedButton.target
from:nil
forEvent:nil];
// Callback should've gotten invoked.
[mockedHandler verify];
}
...@@ -36,6 +36,9 @@ extern NSString* const kConfirmationAlertPrimaryActionAccessibilityIdentifier; ...@@ -36,6 +36,9 @@ extern NSString* const kConfirmationAlertPrimaryActionAccessibilityIdentifier;
// The image. Must be set before the view is loaded. // The image. Must be set before the view is loaded.
@property(nonatomic, strong) UIImage* image; @property(nonatomic, strong) UIImage* image;
// Value to determine whether or not the image's size should be scaled.
@property(nonatomic) BOOL imageHasFixedSize;
// Controls if there is a help button in the view. Must be set before the // Controls if there is a help button in the view. Must be set before the
// view is loaded. // view is loaded.
@property(nonatomic) BOOL helpButtonAvailable; @property(nonatomic) BOOL helpButtonAvailable;
......
...@@ -146,26 +146,33 @@ constexpr CGFloat kSafeAreaMultiplier = 0.8; ...@@ -146,26 +146,33 @@ constexpr CGFloat kSafeAreaMultiplier = 0.8;
self.primaryActionButton = primaryActionButton; self.primaryActionButton = primaryActionButton;
} }
// Constraing the image to the scroll view size and its aspect ratio.
[self.imageView
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
[self.imageView
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisVertical];
CGFloat imageAspectRatio =
self.imageView.image.size.width / self.imageView.image.size.height;
self.scrollViewBottomVerticalConstraint = [scrollView.bottomAnchor self.scrollViewBottomVerticalConstraint = [scrollView.bottomAnchor
constraintLessThanOrEqualToAnchor:scrollViewBottomAnchor]; constraintLessThanOrEqualToAnchor:scrollViewBottomAnchor];
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[self.imageView.widthAnchor
constraintEqualToAnchor:self.imageView.heightAnchor
multiplier:imageAspectRatio],
[scrollView.topAnchor [scrollView.topAnchor
constraintGreaterThanOrEqualToAnchor:topToolbar.bottomAnchor], constraintGreaterThanOrEqualToAnchor:topToolbar.bottomAnchor],
self.scrollViewBottomVerticalConstraint, self.scrollViewBottomVerticalConstraint,
]]; ]];
if (!self.imageHasFixedSize) {
// Constrain the image to the scroll view size and its aspect ratio.
[self.imageView
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:
UILayoutConstraintAxisHorizontal];
[self.imageView
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisVertical];
CGFloat imageAspectRatio =
self.imageView.image.size.width / self.imageView.image.size.height;
[NSLayoutConstraint activateConstraints:@[
[self.imageView.widthAnchor
constraintEqualToAnchor:self.imageView.heightAnchor
multiplier:imageAspectRatio],
]];
}
} }
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
...@@ -343,8 +350,14 @@ constexpr CGFloat kSafeAreaMultiplier = 0.8; ...@@ -343,8 +350,14 @@ constexpr CGFloat kSafeAreaMultiplier = 0.8;
[[UIStackView alloc] initWithArrangedSubviews:subviews]; [[UIStackView alloc] initWithArrangedSubviews:subviews];
[stackView setCustomSpacing:kStackViewSpacingAfterIllustration [stackView setCustomSpacing:kStackViewSpacingAfterIllustration
afterView:self.imageView]; afterView:self.imageView];
if (self.imageHasFixedSize) {
stackView.alignment = UIStackViewAlignmentCenter;
} else {
stackView.alignment = UIStackViewAlignmentFill;
}
stackView.axis = UILayoutConstraintAxisVertical; stackView.axis = UILayoutConstraintAxisVertical;
stackView.alignment = UIStackViewAlignmentFill;
stackView.translatesAutoresizingMaskIntoConstraints = NO; stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.spacing = kStackViewSpacing; stackView.spacing = kStackViewSpacing;
return stackView; return stackView;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment