Commit d9217bcd authored by sdefresne's avatar sdefresne Committed by Commit bot

Upstream Chrome on iOS source code [6/11].

Upstream part of Chrome on iOS source code. Nothing is built yet,
just new files added. The files will be added to the build as part
of the last CL to avoid breaking downstream tree.

BUG=653086

Review-Url: https://codereview.chromium.org/2589803002
Cr-Commit-Position: refs/heads/master@{#439466}
parent 82befc59
// Copyright 2012 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/ntp/new_tab_page_controller.h"
#include <memory>
#include "base/mac/scoped_nsautorelease_pool.h"
#include "base/mac/scoped_nsobject.h"
#include "base/memory/ptr_util.h"
#include "base/message_loop/message_loop.h"
#include "components/bookmarks/test/bookmark_test_helpers.h"
#include "components/prefs/testing_pref_service.h"
#include "components/search_engines/template_url_service.h"
#include "components/sessions/core/tab_restore_service.h"
#include "ios/chrome/browser/bookmarks/bookmark_model_factory.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#include "ios/chrome/browser/chrome_url_constants.h"
#include "ios/chrome/browser/search_engines/template_url_service_factory.h"
#include "ios/chrome/browser/sessions/ios_chrome_tab_restore_service_factory.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_view.h"
#include "ios/chrome/browser/ui/ui_util.h"
#include "ios/chrome/test/block_cleanup_test.h"
#include "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#include "ios/chrome/test/testing_application_context.h"
#include "ios/web/public/test/test_web_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
@interface NewTabPageController (TestSupport)
- (id<NewTabPagePanelProtocol>)currentController;
- (id<NewTabPagePanelProtocol>)bookmarkController;
- (id<NewTabPagePanelProtocol>)incognitoController;
@end
@interface NewTabPageController (PrivateMethods)
@property(nonatomic, retain) NewTabPageView* ntpView;
@end
@implementation NewTabPageController (TestSupport)
- (id<NewTabPagePanelProtocol>)currentController {
return currentController_;
}
- (id<NewTabPagePanelProtocol>)bookmarkController {
return bookmarkController_.get();
}
- (id<NewTabPagePanelProtocol>)incognitoController {
return incognitoController_.get();
}
@end
namespace {
class NewTabPageControllerTest : public BlockCleanupTest {
protected:
void SetUp() override {
BlockCleanupTest::SetUp();
// Set up a test ChromeBrowserState instance.
TestChromeBrowserState::Builder test_cbs_builder;
test_cbs_builder.AddTestingFactory(
IOSChromeTabRestoreServiceFactory::GetInstance(),
IOSChromeTabRestoreServiceFactory::GetDefaultFactory());
test_cbs_builder.AddTestingFactory(
ios::TemplateURLServiceFactory::GetInstance(),
ios::TemplateURLServiceFactory::GetDefaultFactory());
chrome_browser_state_ = test_cbs_builder.Build();
// Load TemplateURLService.
TemplateURLService* template_url_service =
ios::TemplateURLServiceFactory::GetForBrowserState(
chrome_browser_state_.get());
template_url_service->Load();
chrome_browser_state_->CreateBookmarkModel(true);
bookmarks::test::WaitForBookmarkModelToLoad(
ios::BookmarkModelFactory::GetForBrowserState(
chrome_browser_state_.get()));
GURL url(kChromeUINewTabURL);
controller_.reset([[NewTabPageController alloc]
initWithUrl:url
loader:nil
focuser:nil
ntpObserver:nil
browserState:chrome_browser_state_.get()
colorCache:nil
webToolbarDelegate:nil
tabModel:nil]);
incognitoController_.reset([[NewTabPageController alloc]
initWithUrl:url
loader:nil
focuser:nil
ntpObserver:nil
browserState:chrome_browser_state_
->GetOffTheRecordChromeBrowserState()
colorCache:nil
webToolbarDelegate:nil
tabModel:nil]);
};
void TearDown() override {
incognitoController_.reset();
controller_.reset();
// There may be blocks released below that have weak references to |profile|
// owned by chrome_browser_state_. Ensure BlockCleanupTest::TearDown() is
// called before |chrome_browser_state_| is reset.
BlockCleanupTest::TearDown();
chrome_browser_state_.reset();
}
web::TestWebThreadBundle thread_bundle_;
IOSChromeScopedTestingLocalState local_state_;
std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
base::scoped_nsobject<NewTabPageController> controller_;
base::scoped_nsobject<NewTabPageController> incognitoController_;
// The pool has to be the last declared field because it must be destroyed
// first.
base::mac::ScopedNSAutoreleasePool pool_;
};
TEST_F(NewTabPageControllerTest, NewTabBarItemDidChange) {
// Switching the selected index in the NewTabPageBar should cause
// newTabBarItemDidChange to get called.
NewTabPageBar* bar = [[controller_ ntpView] tabBar];
NSUInteger bookmarkIndex = 0;
UIButton* button = [[bar buttons] objectAtIndex:bookmarkIndex];
UIControlEvents event =
IsIPadIdiom() ? UIControlEventTouchDown : UIControlEventTouchUpInside;
[button sendActionsForControlEvents:event];
// Expecting bookmarks panel to be loaded now and to be the current controller
// on iPad but not iPhone.
// Deliberately comparing pointers.
if (IsIPadIdiom()) {
EXPECT_EQ([controller_ currentController],
(id<NewTabPagePanelProtocol>)[controller_ bookmarkController]);
} else {
EXPECT_NE([controller_ currentController],
(id<NewTabPagePanelProtocol>)[controller_ bookmarkController]);
}
}
TEST_F(NewTabPageControllerTest, SelectBookmarkPanel) {
// Expecting on start up that the bookmarkController does not exist.
// Deliberately comparing pointers.
EXPECT_NE([controller_ currentController],
(id<NewTabPagePanelProtocol>)[controller_ bookmarkController]);
// Switching to the Bookmarks panel.
[controller_ selectPanel:NewTabPage::kBookmarksPanel];
// Expecting bookmarks panel to be loaded now and to be the current controller
// on iPad but not iPhone.
// Deliberately comparing pointers.
if (IsIPadIdiom()) {
EXPECT_EQ([controller_ currentController],
(id<NewTabPagePanelProtocol>)[controller_ bookmarkController]);
} else {
EXPECT_NE([controller_ currentController],
(id<NewTabPagePanelProtocol>)[controller_ bookmarkController]);
}
}
TEST_F(NewTabPageControllerTest, SelectIncognitoPanel) {
// Expect on start up that the Incognito panel is the default.
EXPECT_EQ(
(id<NewTabPagePanelProtocol>)[incognitoController_ incognitoController],
[incognitoController_ currentController]);
// Switch to the Bookmarks panel.
[incognitoController_ selectPanel:NewTabPage::kBookmarksPanel];
// Expecting bookmarks panel to be loaded now and to be the current controller
// on iPad but not iPhone.
// Deliberately comparing pointers.
if (IsIPadIdiom()) {
EXPECT_EQ(
[incognitoController_ currentController],
(id<NewTabPagePanelProtocol>)[incognitoController_ bookmarkController]);
} else {
EXPECT_NE(
[incognitoController_ currentController],
(id<NewTabPagePanelProtocol>)[incognitoController_ bookmarkController]);
}
}
TEST_F(NewTabPageControllerTest, TestWantsLocationBarHintText) {
// Default NTP doesn't show location bar hint text on iPad, and it does on
// iPhone.
if (IsIPadIdiom())
EXPECT_EQ(NO, [controller_ wantsLocationBarHintText]);
else
EXPECT_EQ(YES, [controller_ wantsLocationBarHintText]);
// Default incognito always does.
EXPECT_EQ(YES, [incognitoController_ wantsLocationBarHintText]);
}
TEST_F(NewTabPageControllerTest, NewTabPageIdentifierConversion) {
EXPECT_EQ("open_tabs",
NewTabPage::FragmentFromIdentifier(NewTabPage::kOpenTabsPanel));
EXPECT_EQ("", NewTabPage::FragmentFromIdentifier(NewTabPage::kNone));
EXPECT_EQ(NewTabPage::kBookmarksPanel,
NewTabPage::IdentifierFromFragment("bookmarks"));
EXPECT_EQ(NewTabPage::kNone, NewTabPage::IdentifierFromFragment("garbage"));
EXPECT_EQ(NewTabPage::kNone, NewTabPage::IdentifierFromFragment(""));
}
} // anonymous namespace
This diff is collapsed.
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_HEADER_CONSTANTS_H_
#define IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_HEADER_CONSTANTS_H_
#import <CoreGraphics/CoreGraphics.h>
namespace ntp_header {
// The minimum height of the new tab page header view when the new tab page is
// scrolled up.
extern const CGFloat kMinHeaderHeight;
// The scroll distance within which to animate the search field from its
// initial frame to its final full bleed frame.
extern const CGFloat kAnimationDistance;
extern const CGFloat kToolbarHeight;
extern const CGFloat kScrolledToTopOmniboxBottomMargin;
} // namespace ntp_header
#endif // IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_HEADER_CONSTANTS_H_
// Copyright 2014 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/ntp/new_tab_page_header_constants.h"
namespace ntp_header {
const CGFloat kMinHeaderHeight = 62;
const CGFloat kAnimationDistance = 42;
const CGFloat kToolbarHeight = 56;
const CGFloat kScrolledToTopOmniboxBottomMargin = 4;
} // ntp_header
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_HEADER_VIEW_H_
#define IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_HEADER_VIEW_H_
#import <UIKit/UIKit.h>
#import "ios/chrome/browser/ui/toolbar/toolbar_owner.h"
@protocol OmniboxFocuser;
@class TabModel;
@protocol WebToolbarDelegate;
class ReadingListModel;
// Header view for the Material Design NTP. The header view contains all views
// that are displayed above the list of most visited sites, which includes the
// toolbar buttons, Google doodle, and fake omnibox.
@interface NewTabPageHeaderView : UICollectionReusableView<ToolbarOwner>
// Return the toolbar view;
@property(nonatomic, readonly) UIView* toolBarView;
// Creates a NewTabPageToolbarController using the given |toolbarDelegate|,
// |focuser| and |readingListModel|, and adds the toolbar view to self.
- (void)addToolbarWithDelegate:(id<WebToolbarDelegate>)toolbarDelegate
focuser:(id<OmniboxFocuser>)focuser
tabModel:(TabModel*)tabModel
readingListModel:(ReadingListModel*)readingListModel;
// Changes the frame of |searchField| based on its |initialFrame| and the scroll
// view's y |offset|. Also adjust the alpha values for |_searchBoxBorder| and
// |_shadow| and the constant values for the |constraints|.
- (void)updateSearchField:(UIView*)searchField
withInitialFrame:(CGRect)initialFrame
subviewConstraints:(NSArray*)constraints
forOffset:(CGFloat)offset;
// Initializes |_searchBoxBorder| and |_shadow| and adds them to |searchField|.
- (void)addViewsToSearchField:(UIView*)searchField;
// Animates |_shadow|'s alpha to 0.
- (void)fadeOutShadow;
// Hide toolbar subviews that should not be displayed on the new tab page.
- (void)hideToolbarViewsForNewTabPage;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_HEADER_VIEW_H_
// Copyright 2014 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/ntp/new_tab_page_header_view.h"
#include "base/logging.h"
#include "base/mac/scoped_nsobject.h"
#import "ios/chrome/browser/tabs/tab_model.h"
#import "ios/chrome/browser/tabs/tab_model_observer.h"
#import "ios/chrome/browser/ui/image_util.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_toolbar_controller.h"
#import "ios/chrome/common/material_timing.h"
#include "ios/chrome/grit/ios_theme_resources.h"
#include "ui/base/resource/resource_bundle.h"
#import "ui/gfx/ios/uikit_util.h"
namespace {
const CGFloat kOmniboxImageBottomInset = 1;
const CGFloat kHintLabelSidePadding = 12;
const CGFloat kMaxConstraintConstantDiff = 5;
} // namespace
@interface NewTabPageHeaderView ()<TabModelObserver> {
base::scoped_nsobject<NewTabPageToolbarController> _toolbarController;
base::scoped_nsobject<TabModel> _tabModel;
base::scoped_nsobject<UIImageView> _searchBoxBorder;
base::scoped_nsobject<UIImageView> _shadow;
}
@end
@implementation NewTabPageHeaderView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.clipsToBounds = YES;
}
return self;
}
- (void)dealloc {
[_tabModel removeObserver:self];
[super dealloc];
}
- (UIView*)toolBarView {
return [_toolbarController view];
}
- (ToolbarController*)relinquishedToolbarController {
ToolbarController* relinquishedToolbarController = nil;
if ([[_toolbarController view] isDescendantOfView:self]) {
// Only relinquish the toolbar controller if it's in the hierarchy.
relinquishedToolbarController = _toolbarController.get();
}
return relinquishedToolbarController;
}
- (void)reparentToolbarController {
[self addSubview:[_toolbarController view]];
}
- (void)addToolbarWithDelegate:(id<WebToolbarDelegate>)toolbarDelegate
focuser:(id<OmniboxFocuser>)focuser
tabModel:(TabModel*)tabModel
readingListModel:(ReadingListModel*)readingListModel {
DCHECK(!_toolbarController);
DCHECK(focuser);
_toolbarController.reset([[NewTabPageToolbarController alloc]
initWithToolbarDelegate:toolbarDelegate
focuser:focuser]);
_toolbarController.get().readingListModel = readingListModel;
[_tabModel removeObserver:self];
_tabModel.reset([tabModel retain]);
[self addTabModelObserver];
UIView* toolbarView = [_toolbarController view];
CGRect toolbarFrame = self.bounds;
toolbarFrame.size.height = ntp_header::kToolbarHeight;
toolbarView.frame = toolbarFrame;
[toolbarView setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[self hideToolbarViewsForNewTabPage];
[self setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[self addSubview:[_toolbarController view]];
}
- (void)hideToolbarViewsForNewTabPage {
[_toolbarController hideViewsForNewTabPage:YES];
};
- (void)addTabModelObserver {
[_tabModel addObserver:self];
[_toolbarController setTabCount:[_tabModel count]];
}
- (void)addViewsToSearchField:(UIView*)searchField {
[searchField setBackgroundColor:[UIColor whiteColor]];
UIImage* searchBorderImage =
StretchableImageNamed(@"ntp_google_search_box", 12, 12);
_searchBoxBorder.reset([[UIImageView alloc] initWithImage:searchBorderImage]);
[_searchBoxBorder setFrame:[searchField bounds]];
[_searchBoxBorder setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight];
[searchField insertSubview:_searchBoxBorder atIndex:0];
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
UIImage* fullBleedShadow =
rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW_FULL_BLEED).ToUIImage();
_shadow.reset([[UIImageView alloc] initWithImage:fullBleedShadow]);
CGRect shadowFrame = [searchField bounds];
shadowFrame.origin.y =
searchField.bounds.size.height - kOmniboxImageBottomInset;
shadowFrame.size.height = fullBleedShadow.size.height;
[_shadow setFrame:shadowFrame];
[_shadow setUserInteractionEnabled:NO];
[_shadow setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleTopMargin];
[searchField addSubview:_shadow];
[_shadow setAlpha:0];
}
- (void)tabModelDidChangeTabCount:(TabModel*)model {
DCHECK(model == _tabModel);
[_toolbarController setTabCount:[_tabModel count]];
}
- (void)updateSearchField:(UIView*)searchField
withInitialFrame:(CGRect)initialFrame
subviewConstraints:(NSArray*)constraints
forOffset:(CGFloat)offset {
// The scroll offset at which point |searchField|'s frame should stop growing.
CGFloat maxScaleOffset =
self.frame.size.height - ntp_header::kMinHeaderHeight;
// The scroll offset at which point |searchField|'s frame should start
// growing.
CGFloat startScaleOffset = maxScaleOffset - ntp_header::kAnimationDistance;
CGFloat percent = 0;
if (offset > startScaleOffset) {
CGFloat animatingOffset = offset - startScaleOffset;
percent = MIN(1, MAX(0, animatingOffset / ntp_header::kAnimationDistance));
}
// Calculate the amount to grow the width and height of |searchField| so that
// its frame covers the entire toolbar area.
CGFloat maxXInset = ui::AlignValueToUpperPixel(
(initialFrame.size.width - self.bounds.size.width) / 2 - 1);
CGFloat maxYOffset = ui::AlignValueToUpperPixel(
(ntp_header::kToolbarHeight - initialFrame.size.height) / 2 +
kOmniboxImageBottomInset - 0.5);
CGRect searchFieldFrame = CGRectInset(initialFrame, maxXInset * percent, 0);
searchFieldFrame.origin.y += maxYOffset * percent;
searchFieldFrame.size.height += 2 * maxYOffset * percent;
[searchField setFrame:CGRectIntegral(searchFieldFrame)];
[_searchBoxBorder setAlpha:(1 - percent)];
[_shadow setAlpha:percent];
// Adjust the position of the search field's subviews by adjusting their
// constraint constant value.
CGFloat constantDiff = percent * kMaxConstraintConstantDiff;
for (NSLayoutConstraint* constraint in constraints) {
if (constraint.constant > 0)
constraint.constant = constantDiff + kHintLabelSidePadding;
else
constraint.constant = -constantDiff;
}
}
- (void)fadeOutShadow {
[UIView animateWithDuration:ios::material::kDuration1
animations:^{
[_shadow setAlpha:0];
}];
}
@end
// Copyright 2012 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_PANEL_PROTOCOL_H_
#define IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_PANEL_PROTOCOL_H_
#import <UIKit/UIKit.h>
@protocol NewTabPagePanelProtocol;
extern const int kNewTabPageShadowHeight;
extern const int kNewTabPageDistanceToFadeShadow;
@protocol NewTabPagePanelControllerDelegate<NSObject>
// Updates the NTP bar shadow alpha for the given NewTabPagePanelProtocol.
- (void)updateNtpBarShadowForPanelController:
(id<NewTabPagePanelProtocol>)ntpPanelController;
@end
// TODO(jbbegue): rename, extract and upstream so that CRWNativeContent can
// implement it ( https://crbug.com/492156 ).
@protocol NewTabPagePanelControllerSnapshotting<NSObject>
@optional
// Called when a snapshot of the content will be taken.
- (void)willUpdateSnapshot;
@end
// Base class of a controller for the panels in the New Tab Page. This should
// not be instantiated, but instead one of its sub-classes.
@protocol NewTabPagePanelProtocol<NewTabPagePanelControllerSnapshotting>
// NewTabPagePanelController delegate, may be nil.
@property(nonatomic, assign) id<NewTabPagePanelControllerDelegate> delegate;
// Alpha value to use for the NewTabPageBar shadow.
@property(nonatomic, readonly) CGFloat alphaForBottomShadow;
// Main view.
@property(nonatomic, readonly) UIView* view;
// Reload any displayed data to ensure the view is up to date.
- (void)reload;
// Notifies the NewTabPagePanelProtocol that it has been shown.
- (void)wasShown;
// Notifies the NewTabPagePanelProtocol that it has been hidden.
- (void)wasHidden;
// Dismisses any modal interaction elements.
- (void)dismissModals;
// Dismisses on-screen keyboard if necessary.
- (void)dismissKeyboard;
// Disable and enable scrollToTop
- (void)setScrollsToTop:(BOOL)enable;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_PANEL_PROTOCOL_H_
// Copyright 2012 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/ntp/new_tab_page_panel_protocol.h"
const int kNewTabPageShadowHeight = 2;
const int kNewTabPageDistanceToFadeShadow = 20;
// Copyright 2012 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 "base/test/ios/wait_util.h"
#include "ios/chrome/browser/sessions/ios_chrome_tab_restore_service_factory.h"
#import "ios/chrome/browser/tabs/tab_model.h"
#include "ios/chrome/browser/test/perf_test_with_bvc_ios.h"
#import "ios/chrome/browser/ui/browser_view_controller.h"
#import "ios/chrome/browser/ui/browser_view_controller_dependency_factory.h"
namespace {
const NSTimeInterval kMaxUICatchupDelay = 2.0; // seconds
class NewTabPagePerfTest : public PerfTestWithBVC {
public:
NewTabPagePerfTest()
: PerfTestWithBVC("NTP - Create",
"First Tab",
"",
false,
false,
true,
10) {}
protected:
void SetUp() override {
PerfTestWithBVC::SetUp();
IOSChromeTabRestoreServiceFactory::GetInstance()->SetTestingFactory(
chrome_browser_state_.get(),
IOSChromeTabRestoreServiceFactory::GetDefaultFactory());
}
base::TimeDelta TimedNewTab() {
base::Time startTime = base::Time::NowFromSystemTime();
[bvc_ newTab:nil];
return base::Time::NowFromSystemTime() - startTime;
}
void SettleUI() {
base::test::ios::WaitUntilCondition(
nil, nullptr, base::TimeDelta::FromSecondsD(kMaxUICatchupDelay));
}
};
// Output format first test:
// [*]RESULT NTP - Create: NTP Gentle Create First Tab= number ms
// Output format subsequent average:
// [*]RESULT NTP - Create: NTP Gentle Create= number ms
TEST_F(NewTabPagePerfTest, OpenNTP_Gentle) {
RepeatTimedRuns("NTP Gentle Create",
^(int index) {
return TimedNewTab();
},
^{
SettleUI();
});
}
// Output format first test:
// [*]RESULT NTP - Create: NTP Hammer Create First Tab= number ms
// Output format subsequent average:
// [*]RESULT NTP - Create: NTP Hammer Create= number ms
TEST_F(NewTabPagePerfTest, OpenNTP_Hammer) {
RepeatTimedRuns("NTP Hammer Create",
^(int index) {
return TimedNewTab();
},
nil);
// Allows the run loops to run before teardown.
SettleUI();
}
} // anonymous namespace
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_TOOLBAR_CONTROLLER_H_
#define IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_TOOLBAR_CONTROLLER_H_
#import "ios/chrome/browser/ui/toolbar/toolbar_controller.h"
@protocol OmniboxFocuser;
@protocol WebToolbarDelegate;
// New tab page specific toolbar. The background view is hidden and the
// navigation buttons are also hidden if there is no forward history. Does not
// contain an omnibox but tapping in the center will focus the main toolbar's
// omnibox.
@interface NewTabPageToolbarController : ToolbarController
// Designated initializer. The underlying ToolbarController is initialized with
// ToolbarControllerStyleLightMode.
- (instancetype)initWithToolbarDelegate:(id<WebToolbarDelegate>)delegate
focuser:(id<OmniboxFocuser>)focuser;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_TOOLBAR_CONTROLLER_H_
// Copyright 2014 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/ntp/new_tab_page_toolbar_controller.h"
#include "base/logging.h"
#include "base/mac/scoped_nsobject.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "components/strings/grit/components_strings.h"
#include "components/toolbar/toolbar_model.h"
#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
#import "ios/chrome/browser/ui/rtl_geometry.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_model_ios.h"
#include "ios/chrome/browser/ui/toolbar/toolbar_resource_macros.h"
#import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ui/base/l10n/l10n_util.h"
using base::UserMetricsAction;
namespace {
const CGFloat kButtonYOffset = 4.0;
const CGFloat kBackButtonLeading = 0;
const CGFloat kForwardButtonLeading = 48;
const CGFloat kOmniboxFocuserLeading = 96;
const CGSize kBackButtonSize = {48, 48};
const CGSize kForwardButtonSize = {48, 48};
const CGSize kOmniboxFocuserSize = {128, 48};
enum {
NTPToolbarButtonNameBack = NumberOfToolbarButtonNames,
NTPToolbarButtonNameForward,
NumberOfNTPToolbarButtonNames,
};
} // namespace
@interface NewTabPageToolbarController () {
base::scoped_nsobject<UIButton> _backButton;
base::scoped_nsobject<UIButton> _forwardButton;
base::scoped_nsobject<UIButton> _omniboxFocuser;
id<WebToolbarDelegate> _delegate;
// Delegate to focus and blur the omnibox.
base::WeakNSProtocol<id<OmniboxFocuser>> _focuser;
}
@end
@implementation NewTabPageToolbarController
- (instancetype)initWithToolbarDelegate:(id<WebToolbarDelegate>)delegate
focuser:(id<OmniboxFocuser>)focuser {
self = [super initWithStyle:ToolbarControllerStyleLightMode];
if (self) {
_delegate = delegate;
_focuser.reset(focuser);
[self.backgroundView setHidden:YES];
CGFloat boundingWidth = self.view.bounds.size.width;
LayoutRect backButtonLayout =
LayoutRectMake(kBackButtonLeading, boundingWidth, kButtonYOffset,
kBackButtonSize.width, kBackButtonSize.height);
_backButton.reset(
[[UIButton alloc] initWithFrame:LayoutRectGetRect(backButtonLayout)]);
[_backButton
setAutoresizingMask:UIViewAutoresizingFlexibleTrailingMargin() |
UIViewAutoresizingFlexibleBottomMargin];
LayoutRect forwardButtonLayout =
LayoutRectMake(kForwardButtonLeading, boundingWidth, kButtonYOffset,
kForwardButtonSize.width, kForwardButtonSize.height);
_forwardButton.reset([[UIButton alloc]
initWithFrame:LayoutRectGetRect(forwardButtonLayout)]);
[_forwardButton
setAutoresizingMask:UIViewAutoresizingFlexibleTrailingMargin() |
UIViewAutoresizingFlexibleBottomMargin];
LayoutRect omniboxFocuserLayout =
LayoutRectMake(kOmniboxFocuserLeading, boundingWidth, kButtonYOffset,
kOmniboxFocuserSize.width, kOmniboxFocuserSize.height);
_omniboxFocuser.reset([[UIButton alloc]
initWithFrame:LayoutRectGetRect(omniboxFocuserLayout)]);
[_omniboxFocuser
setAccessibilityLabel:l10n_util::GetNSString(IDS_ACCNAME_LOCATION)];
[_omniboxFocuser setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[self.view addSubview:_backButton];
[self.view addSubview:_forwardButton];
[self.view addSubview:_omniboxFocuser];
[_backButton setImageEdgeInsets:UIEdgeInsetsMakeDirected(0, 0, 0, -10)];
[_forwardButton setImageEdgeInsets:UIEdgeInsetsMakeDirected(0, -7, 0, 0)];
// Set up the button images.
[self setUpButton:_backButton
withImageEnum:NTPToolbarButtonNameBack
forInitialState:UIControlStateDisabled
hasDisabledImage:YES
synchronously:NO];
[self setUpButton:_forwardButton
withImageEnum:NTPToolbarButtonNameForward
forInitialState:UIControlStateDisabled
hasDisabledImage:YES
synchronously:NO];
base::scoped_nsobject<UILongPressGestureRecognizer> backLongPress(
[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleLongPress:)]);
[_backButton addGestureRecognizer:backLongPress];
base::scoped_nsobject<UILongPressGestureRecognizer> forwardLongPress(
[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleLongPress:)]);
[_forwardButton addGestureRecognizer:forwardLongPress];
[_backButton setTag:IDC_BACK];
[_forwardButton setTag:IDC_FORWARD];
[_omniboxFocuser addTarget:self
action:@selector(focusOmnibox:)
forControlEvents:UIControlEventTouchUpInside];
SetA11yLabelAndUiAutomationName(_backButton, IDS_ACCNAME_BACK, @"Back");
SetA11yLabelAndUiAutomationName(_forwardButton, IDS_ACCNAME_FORWARD,
@"Forward");
}
return self;
}
- (CGFloat)statusBarOffset {
return 0;
}
- (BOOL)imageShouldFlipForRightToLeftLayoutDirection:(int)imageEnum {
DCHECK(imageEnum < NumberOfNTPToolbarButtonNames);
if (imageEnum < NumberOfToolbarButtonNames)
return [super imageShouldFlipForRightToLeftLayoutDirection:imageEnum];
if (imageEnum == NTPToolbarButtonNameBack ||
imageEnum == NTPToolbarButtonNameForward) {
return YES;
}
return NO;
}
- (int)imageEnumForButton:(UIButton*)button {
if (button == _backButton.get())
return NTPToolbarButtonNameBack;
if (button == _forwardButton.get())
return NTPToolbarButtonNameForward;
return [super imageEnumForButton:button];
}
- (int)imageIdForImageEnum:(int)index
style:(ToolbarControllerStyle)style
forState:(ToolbarButtonUIState)state {
DCHECK(style < ToolbarControllerStyleMaxStyles);
DCHECK(state < NumberOfToolbarButtonUIStates);
if (index >= NumberOfNTPToolbarButtonNames)
NOTREACHED();
if (index < NumberOfToolbarButtonNames)
return [super imageIdForImageEnum:index style:style forState:state];
index -= NumberOfToolbarButtonNames;
const int numberOfAddedNames =
NumberOfNTPToolbarButtonNames - NumberOfToolbarButtonNames;
// Name, style [light, dark], UIControlState [normal, pressed, disabled]
static int
buttonImageIds[numberOfAddedNames][2][NumberOfToolbarButtonUIStates] = {
TOOLBAR_IDR_THREE_STATE(BACK), TOOLBAR_IDR_THREE_STATE(FORWARD),
};
return buttonImageIds[index][style][state];
}
- (IBAction)recordUserMetrics:(id)sender {
if (sender == _backButton.get()) {
base::RecordAction(UserMetricsAction("MobileToolbarBack"));
} else if (sender == _forwardButton.get()) {
base::RecordAction(UserMetricsAction("MobileToolbarForward"));
} else {
[super recordUserMetrics:sender];
}
}
- (void)handleLongPress:(UILongPressGestureRecognizer*)gesture {
if (gesture.state != UIGestureRecognizerStateBegan)
return;
if (gesture.view == _backButton.get()) {
base::scoped_nsobject<GenericChromeCommand> command(
[[GenericChromeCommand alloc] initWithTag:IDC_SHOW_BACK_HISTORY]);
[_backButton chromeExecuteCommand:command];
} else if (gesture.view == _forwardButton.get()) {
base::scoped_nsobject<GenericChromeCommand> command(
[[GenericChromeCommand alloc] initWithTag:IDC_SHOW_FORWARD_HISTORY]);
[_forwardButton chromeExecuteCommand:command];
}
}
- (void)hideViewsForNewTabPage:(BOOL)hide {
[super hideViewsForNewTabPage:hide];
// Show the back/forward buttons if there is forward history.
ToolbarModelIOS* toolbarModelIOS = [_delegate toolbarModelIOS];
BOOL forwardEnabled = toolbarModelIOS->CanGoForward();
[_backButton setHidden:!forwardEnabled && hide];
[_backButton setEnabled:toolbarModelIOS->CanGoBack()];
[_forwardButton setHidden:!forwardEnabled && hide];
}
- (void)focusOmnibox:(id)sender {
[_focuser focusFakebox];
}
- (IBAction)stackButtonTouchDown:(id)sender {
[_delegate prepareToEnterTabSwitcher:self];
}
@end
// Copyright 2012 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_VIEW_H_
#define IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_VIEW_H_
#import <UIKit/UIKit.h>
@class NewTabPageBar;
// Container view for the new tab page so that the subviews don't get directly
// accessed from the view controller.
@interface NewTabPageView : UIView
@property(nonatomic, weak, readonly) UIScrollView* scrollView;
@property(nonatomic, weak, readonly) NewTabPageBar* tabBar;
- (instancetype)initWithFrame:(CGRect)frame
andScrollView:(UIScrollView*)scrollView
andTabBar:(NewTabPageBar*)tabBar NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
// initWithCoder would only be needed for building this view through .xib files.
- (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE;
// Updates scrollView's content size to accommodate its panels.
- (void)updateScrollViewContentSize;
// Returns the frame of the item's view within the scrollView's content.
// |index| must be a valid index for tabBar's |items|.
- (CGRect)panelFrameForItemAtIndex:(NSUInteger)index;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_NEW_TAB_PAGE_VIEW_H_
// Copyright 2012 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/ntp/new_tab_page_view.h"
#include "base/logging.h"
#include "base/mac/objc_property_releaser.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_bar.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_bar_item.h"
#import "ios/chrome/browser/ui/rtl_geometry.h"
#include "ios/chrome/browser/ui/ui_util.h"
@implementation NewTabPageView {
@private
// The objects pointed to by |tabBar_| and |scrollView_| are owned as
// subviews already.
__unsafe_unretained NewTabPageBar* tabBar_; // weak
__unsafe_unretained UIScrollView* scrollView_; // weak
base::mac::ObjCPropertyReleaser propertyReleaser_NewTabPageView_;
}
@synthesize scrollView = scrollView_;
@synthesize tabBar = tabBar_;
- (instancetype)initWithFrame:(CGRect)frame
andScrollView:(UIScrollView*)scrollView
andTabBar:(NewTabPageBar*)tabBar {
self = [super initWithFrame:frame];
if (self) {
propertyReleaser_NewTabPageView_.Init(self, [NewTabPageView class]);
[self addSubview:scrollView];
[self addSubview:tabBar];
scrollView_ = scrollView;
tabBar_ = tabBar;
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
NOTREACHED();
return nil;
}
- (instancetype)initWithCoder:(NSCoder*)aDecoder {
NOTREACHED();
return nil;
}
- (void)setFrame:(CGRect)frame {
// When transitioning the iPhone xib to an iPad idiom, the setFrame call below
// can sometimes fire a scrollViewDidScroll event which changes the
// selectedIndex underneath us. Save the selected index and remove the
// delegate so scrollViewDidScroll isn't called. Then fix the scrollView
// offset after updating the frame.
NSUInteger selectedIndex = self.tabBar.selectedIndex;
id<UIScrollViewDelegate> delegate = self.scrollView.delegate;
self.scrollView.delegate = nil;
[super setFrame:frame];
self.scrollView.delegate = delegate;
// Set the scrollView content size.
[self updateScrollViewContentSize];
// Set the frame of the laid out NTP panels on iPad.
if (IsIPadIdiom()) {
NSUInteger index = 0;
CGFloat selectedItemXOffset = 0;
for (NewTabPageBarItem* item in self.tabBar.items) {
item.view.frame = [self panelFrameForItemAtIndex:index];
if (index == selectedIndex)
selectedItemXOffset = CGRectGetMinX(item.view.frame);
index++;
}
// Ensure the selected NTP panel is lined up correctly when the frame
// changes.
CGPoint point = CGPointMake(selectedItemXOffset, 0);
[self.scrollView setContentOffset:point animated:NO];
}
// Trigger a layout. The |-layoutIfNeeded| call is required because sometimes
// |-layoutSubviews| is not successfully triggered when |-setNeedsLayout| is
// called after frame changes due to autoresizing masks.
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.tabBar.hidden = !self.tabBar.items.count;
if (self.tabBar.hidden) {
self.scrollView.frame = self.bounds;
} else {
CGSize barSize = [self.tabBar sizeThatFits:self.bounds.size];
self.tabBar.frame = CGRectMake(CGRectGetMinX(self.bounds),
CGRectGetMaxY(self.bounds) - barSize.height,
barSize.width, barSize.height);
self.scrollView.frame = CGRectMake(
CGRectGetMinX(self.bounds), CGRectGetMinY(self.bounds),
CGRectGetWidth(self.bounds), CGRectGetMinY(self.tabBar.frame));
}
}
- (void)updateScrollViewContentSize {
CGSize contentSize = self.scrollView.bounds.size;
// On iPhone, NTP doesn't scroll horizontally, as alternate panels are shown
// modally. On iPad, panels are laid out side by side in the scroll view.
if (IsIPadIdiom()) {
contentSize.width *= self.tabBar.items.count;
}
self.scrollView.contentSize = contentSize;
}
- (CGRect)panelFrameForItemAtIndex:(NSUInteger)index {
CGRect contentBounds = CGRectMake(0, 0, self.scrollView.contentSize.width,
self.scrollView.contentSize.height);
LayoutRect layout =
LayoutRectForRectInBoundingRect(self.scrollView.bounds, contentBounds);
layout.position.leading = layout.size.width * index;
return LayoutRectGetRect(layout);
}
@end
// Copyright (c) 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_NOTIFICATION_PROMO_WHATS_NEW_H_
#define IOS_CHROME_BROWSER_UI_NTP_NOTIFICATION_PROMO_WHATS_NEW_H_
#include <string>
#include <vector>
#include "base/macros.h"
#include "ios/chrome/browser/notification_promo.h"
#include "ios/public/provider/chrome/browser/images/whats_new_icon.h"
#include "url/gurl.h"
namespace base {
class DictionaryValue;
}
// Helper class for NotificationPromo that deals with mobile_ntp promos.
class NotificationPromoWhatsNew {
public:
explicit NotificationPromoWhatsNew(PrefService* local_state);
~NotificationPromoWhatsNew();
// Initialize from variations/prefs/JSON.
// Return true if the mobile NTP promotion is valid.
bool Init();
// Used by experimental setting to always show a promo.
bool ClearAndInitFromJson(const base::DictionaryValue& json);
// Return true if the promo is valid and can be shown.
bool CanShow() const;
// Mark the promo as closed when the user dismisses it.
void HandleClosed();
// Mark the promo as having been viewed.
void HandleViewed();
bool valid() const { return valid_; }
const std::string& promo_type() { return promo_type_; }
const std::string& promo_text() { return promo_text_; }
const std::string& promo_name() { return promo_name_; }
WhatsNewIcon icon() { return icon_; }
bool IsURLPromo() const;
const GURL& url() { return url_; }
bool IsChromeCommand() const;
int command_id() { return command_id_; }
private:
// Initialize the state and validity from the low-level notification_promo_.
bool InitFromNotificationPromo();
// Inject a fake promo. The parameters are equivalent to the equivalent
// parameters that can be provided by the variations API. In addition, for
// some variations parameters that are not in this list, the following
// defaults are used: start: 1 Jan 1999 0:26:06 GMT,
// end: 1 Jan 2199 0:26:06 GMT, max_views: 20, max_seconds: 259200.
void InjectFakePromo(const std::string& promo_id,
const std::string& promo_text,
const std::string& promo_type,
const std::string& command,
const std::string& url,
const std::string& metric_name,
const std::string& icon);
// Prefs service for promos.
PrefService* local_state_;
// True if InitFromPrefs/JSON was called and all mandatory fields were found.
bool valid_;
// Text of promo.
std::string promo_text_;
// Type of whats new promo.
std::string promo_type_;
// Name of promo.
std::string promo_name_;
// Icon of promo.
WhatsNewIcon icon_;
// The minimum number of seconds from installation before promo can be valid.
// E.g. Don't show the promo if installation was within N days.
int seconds_since_install_;
// The duration after installation that the promo can be valid.
// E.g. Don't show the promo if installation was more than N days ago.
int max_seconds_since_install_;
// If promo type is 'url'.
GURL url_;
// If promo type is 'chrome_command'.
int command_id_;
// Metric name to append
std::string metric_name_;
// The lower-level notification promo.
ios::NotificationPromo notification_promo_;
// Convert an icon name string to WhatsNewIcon.
WhatsNewIcon ParseIconName(const std::string& icon_name);
DISALLOW_COPY_AND_ASSIGN(NotificationPromoWhatsNew);
};
#endif // IOS_CHROME_BROWSER_UI_NTP_NOTIFICATION_PROMO_WHATS_NEW_H_
This diff is collapsed.
// Copyright (c) 2016 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/ntp/notification_promo_whats_new.h"
#include <map>
#include "base/metrics/field_trial.h"
#include "base/test/user_action_tester.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "components/variations/variations_associated_data.h"
#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
#include "ios/chrome/grit/ios_chromium_strings.h"
#include "ios/public/provider/chrome/browser/images/whats_new_icon.h"
#include "testing/platform_test.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
namespace {
// Test fixture for NotificationPromoWhatsNew.
class NotificationPromoWhatsNewTest : public PlatformTest {
public:
NotificationPromoWhatsNewTest()
: promo_(&local_state_),
field_trial_list_(new base::FieldTrialList(NULL)) {
ios::NotificationPromo::RegisterPrefs(local_state_.registry());
local_state_.registry()->RegisterInt64Pref(metrics::prefs::kInstallDate, 0);
}
~NotificationPromoWhatsNewTest() override {
variations::testing::ClearAllVariationParams();
}
void TearDown() override {
promo_.ClearAndInitFromJson(base::DictionaryValue());
PlatformTest::TearDown();
}
// Sets up a mock finch trial and inits the NotificationPromoWhatsNew. All
// parameters will be added to the list of finch parameters.
void Init(const std::string& start,
const std::string& end,
const std::string& promo_text,
const std::string& promo_id,
const std::string& promo_type,
const std::string& url,
const std::string& command,
const std::string& metric_name,
const std::string& icon,
const std::string& seconds_since_install,
const std::string& max_seconds_since_install) {
std::map<std::string, std::string> field_trial_params;
field_trial_params["start"] = start;
field_trial_params["end"] = end;
field_trial_params["promo_text"] = promo_text;
field_trial_params["promo_id"] = promo_id;
field_trial_params["promo_type"] = promo_type;
field_trial_params["url"] = url;
field_trial_params["command"] = command;
field_trial_params["metric_name"] = metric_name;
field_trial_params["icon"] = icon;
field_trial_params["seconds_since_install"] = seconds_since_install;
field_trial_params["max_seconds_since_install"] = max_seconds_since_install;
variations::AssociateVariationParams("IOSNTPPromotion", "Group1",
field_trial_params);
base::FieldTrialList::CreateFieldTrial("IOSNTPPromotion", "Group1");
promo_.Init();
}
// Tests that |promo_text|, |promo_type|, |url|, |command_id|, and |icon|
// equal their respective values in |promo_|, and that |valid| matches the
// return value of |promo_|'s |CanShow()| method. |icon| is verified only if
// |valid| is true.
void RunTests(const std::string& promo_text,
const std::string& promo_type,
const std::string& url,
int command_id,
WhatsNewIcon icon,
bool valid) {
EXPECT_EQ(promo_text, promo_.promo_text());
EXPECT_EQ(promo_type, promo_.promo_type());
if (promo_type == "url")
EXPECT_EQ(url, promo_.url().spec());
else
EXPECT_EQ(command_id, promo_.command_id());
EXPECT_EQ(valid, promo_.CanShow());
// |icon()| is set only if the promo is valid.
if (valid)
EXPECT_EQ(icon, promo_.icon());
}
protected:
TestingPrefServiceSimple local_state_;
NotificationPromoWhatsNew promo_;
private:
std::unique_ptr<base::FieldTrialList> field_trial_list_;
};
// Test that a command-based, valid promo is shown with the correct text.
TEST_F(NotificationPromoWhatsNewTest, NotificationPromoCommandTest) {
Init("3 Aug 1999 9:26:06 GMT", "3 Aug 2199 9:26:06 GMT",
"IDS_IOS_APP_RATING_PROMO_STRING", "0", "chrome_command", "",
"ratethisapp", "RateThisAppPromo", "logo", "0", "0");
RunTests(l10n_util::GetStringUTF8(IDS_IOS_APP_RATING_PROMO_STRING),
"chrome_command", "", IDC_RATE_THIS_APP, WHATS_NEW_LOGO, true);
}
// Test that a url-based, valid promo is shown with the correct text and icon.
TEST_F(NotificationPromoWhatsNewTest, NotificationPromoURLTest) {
Init("3 Aug 1999 9:26:06 GMT", "3 Aug 2199 9:26:06 GMT", "Test URL", "0",
"url", "http://blog.chromium.org", "", "TestURLPromo", "", "0", "0");
RunTests("Test URL", "url", "http://blog.chromium.org/", 0, WHATS_NEW_INFO,
true);
}
// Test that an invalid promo is not shown.
TEST_F(NotificationPromoWhatsNewTest, NotificationPromoInvalidTest) {
Init("3 Aug 1999 9:26:06 GMT", "3 Aug 2199 9:26:06 GMT", "Test URL", "0",
"url", "", "", "TestURLPromo", "", "0", "0");
RunTests("Test URL", "url", "", 0, WHATS_NEW_INFO, false);
}
// Test that if max_seconds_since_install is set, and the current time is before
// the cut off, the promo still shows.
TEST_F(NotificationPromoWhatsNewTest, MaxSecondsSinceInstallSuccessTest) {
// Init with max_seconds_since_install set to 2 days.
Init("3 Aug 1999 9:26:06 GMT", "3 Aug 2199 9:26:06 GMT",
"IDS_IOS_APP_RATING_PROMO_STRING", "0", "chrome_command", "",
"ratethisapp", "RateThisAppPromo", "logo", "0", "172800");
// Set install date to one day before now.
base::Time one_day_before_now_time =
base::Time::Now() - base::TimeDelta::FromDays(1);
int64_t one_day_before_now = one_day_before_now_time.ToTimeT();
local_state_.SetInt64(metrics::prefs::kInstallDate, one_day_before_now);
// Expect the promo to show since install date was one day ago, and the promo
// can show until 2 days after install date.
EXPECT_TRUE(promo_.CanShow());
}
// Test that if max_seconds_since_install is set, and the current time is after
// the cut off, the promo does not show.
TEST_F(NotificationPromoWhatsNewTest, MaxSecondsSinceInstallFailureTest) {
// Init with max_seconds_since_install set to 2 days.
Init("3 Aug 1999 9:26:06 GMT", "3 Aug 2199 9:26:06 GMT",
"IDS_IOS_APP_RATING_PROMO_STRING", "0", "chrome_command", "",
"ratethisapp", "RateThisAppPromo", "logo", "0", "172800");
// Set install date to three days before now.
base::Time three_days_before_now_time =
base::Time::Now() - base::TimeDelta::FromDays(3);
int64_t three_days_before_now = three_days_before_now_time.ToTimeT();
local_state_.SetInt64(metrics::prefs::kInstallDate, three_days_before_now);
// Expect the promo not to show since install date was three days ago, and
// the promo can show until 2 days after install date.
EXPECT_FALSE(promo_.CanShow());
}
// Test that if seconds_since_install is set, and the current time is after
// install_date + seconds_since_install, the promo still shows.
TEST_F(NotificationPromoWhatsNewTest, SecondsSinceInstallSuccessTest) {
// Init with seconds_since_install set to 2 days.
Init("3 Aug 1999 9:26:06 GMT", "3 Aug 2199 9:26:06 GMT",
"IDS_IOS_APP_RATING_PROMO_STRING", "0", "chrome_command", "",
"ratethisapp", "RateThisAppPromo", "logo", "172800", "0");
// Set install date to three days before now.
base::Time three_days_before_now_time =
base::Time::Now() - base::TimeDelta::FromDays(3);
int64_t three_days_before_now = three_days_before_now_time.ToTimeT();
local_state_.SetInt64(metrics::prefs::kInstallDate, three_days_before_now);
// Expect the promo to show since install date was three days ago, and the
// promo can show starting at 2 days after install date.
EXPECT_TRUE(promo_.CanShow());
}
// Test that if seconds_since_install is set, and the current time is before
// install_date + seconds_since_install, the promo does not show.
TEST_F(NotificationPromoWhatsNewTest, SecondsSinceInstallFailureTest) {
// Init with seconds_since_install set to 2 days.
Init("3 Aug 1999 9:26:06 GMT", "3 Aug 2199 9:26:06 GMT",
"IDS_IOS_APP_RATING_PROMO_STRING", "0", "chrome_command", "",
"ratethisapp", "RateThisAppPromo", "logo", "172800", "0");
// Set install date to one day before now.
base::Time one_day_before_now_time =
base::Time::Now() - base::TimeDelta::FromDays(1);
int64_t one_day_before_now = one_day_before_now_time.ToTimeT();
local_state_.SetInt64(metrics::prefs::kInstallDate, one_day_before_now);
// Expect the promo not to show since install date was one day ago, and
// the promo can show starting at 2 days after install date.
EXPECT_FALSE(promo_.CanShow());
}
// Test that user actions are recorded when promo is viewed and closed.
TEST_F(NotificationPromoWhatsNewTest, NotificationPromoMetricTest) {
Init("3 Aug 1999 9:26:06 GMT", "3 Aug 2199 9:26:06 GMT",
"IDS_IOS_APP_RATING_PROMO_STRING", "0", "chrome_command", "",
"ratethisapp", "RateThisAppPromo", "logo", "0", "0");
base::UserActionTester user_action_tester;
// Assert that promo is appropriately set up to be viewed.
ASSERT_TRUE(promo_.CanShow());
promo_.HandleViewed();
EXPECT_EQ(1, user_action_tester.GetActionCount(
"WhatsNewPromoViewed_RateThisAppPromo"));
// Verify that the promo closed user action count is 0 before |HandleClosed()|
// is called.
EXPECT_EQ(0, user_action_tester.GetActionCount(
"WhatsNewPromoClosed_RateThisAppPromo"));
promo_.HandleClosed();
EXPECT_EQ(1, user_action_tester.GetActionCount(
"WhatsNewPromoClosed_RateThisAppPromo"));
}
} // namespace
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_BRIDGES_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_BRIDGES_H_
#import <UIKit/UIKit.h>
#import "base/ios/weak_nsobject.h"
#include "base/macros.h"
#include "components/sessions/core/tab_restore_service_observer.h"
@class RecentTabsPanelController;
namespace recent_tabs {
// Bridge class to forward events from the sessions::TabRestoreService to
// Objective-C class RecentTabsPanelController.
class ClosedTabsObserverBridge : public sessions::TabRestoreServiceObserver {
public:
explicit ClosedTabsObserverBridge(RecentTabsPanelController* owner);
~ClosedTabsObserverBridge() override;
// sessions::TabRestoreServiceObserver implementation.
void TabRestoreServiceChanged(sessions::TabRestoreService* service) override;
void TabRestoreServiceDestroyed(
sessions::TabRestoreService* service) override;
private:
base::WeakNSObject<RecentTabsPanelController> owner_;
DISALLOW_COPY_AND_ASSIGN(ClosedTabsObserverBridge);
};
} // namespace recent_tabs
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_BRIDGES_H_
// Copyright 2014 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/ntp/recent_tabs/recent_tabs_bridges.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_table_view_controller.h"
namespace recent_tabs {
#pragma mark - ClosedTabsObserverBridge
ClosedTabsObserverBridge::ClosedTabsObserverBridge(
RecentTabsPanelController* owner)
: owner_(owner) {}
ClosedTabsObserverBridge::~ClosedTabsObserverBridge() {}
void ClosedTabsObserverBridge::TabRestoreServiceChanged(
sessions::TabRestoreService* service) {
[owner_ reloadClosedTabsList];
}
void ClosedTabsObserverBridge::TabRestoreServiceDestroyed(
sessions::TabRestoreService* service) {
[owner_ tabRestoreServiceDestroyed];
}
} // namespace recent_tabs
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_PANEL_CONTROLLER_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_PANEL_CONTROLLER_H_
#import <UIKit/UIKit.h>
#import "ios/chrome/browser/ui/ntp/new_tab_page_panel_protocol.h"
namespace ios {
class ChromeBrowserState;
}
@class RecentTabsTableViewController;
@protocol UrlLoader;
// This is the controller for the Recent Tabs panel on the New Tab Page.
// RecentTabsPanelController controls the RecentTabTableViewDataSource, based on
// the user's signed-in and chrome-sync states.
//
// RecentTabsPanelController listens for notifications about Chrome Sync
// and ChromeToDevice and changes/updates the view accordingly.
//
@interface RecentTabsPanelController : NSObject<NewTabPagePanelProtocol>
// Public initializer.
- (instancetype)initWithLoader:(id<UrlLoader>)loader
browserState:(ios::ChromeBrowserState*)browserState;
// Private initializer, exposed for testing.
- (instancetype)initWithController:(RecentTabsTableViewController*)controller
browserState:(ios::ChromeBrowserState*)browserState
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
// Mark super designated initializer as unavailable.
- (instancetype)initWithNibNamed:(NSString*)nibName NS_UNAVAILABLE;
// Reloads the closed tab list and updates the content of the tableView.
- (void)reloadClosedTabsList;
// Reloads the session data and updates the content of the tableView.
- (void)reloadSessions;
// Sets the tab restore service to null.
- (void)tabRestoreServiceDestroyed;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_PANEL_CONTROLLER_H_
// Copyright 2014 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/ntp/recent_tabs/recent_tabs_panel_controller.h"
#include <memory>
#include "base/mac/scoped_nsobject.h"
#include "components/browser_sync/profile_sync_service.h"
#include "components/sessions/core/tab_restore_service.h"
#include "components/signin/core/browser/signin_manager.h"
#include "components/sync_sessions/open_tabs_ui_delegate.h"
#include "components/sync_sessions/synced_session.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/sessions/ios_chrome_tab_restore_service_factory.h"
#include "ios/chrome/browser/signin/signin_manager_factory.h"
#include "ios/chrome/browser/sync/ios_chrome_profile_sync_service_factory.h"
#include "ios/chrome/browser/sync/sync_setup_service.h"
#include "ios/chrome/browser/sync/sync_setup_service_factory.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_bridges.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_table_view_controller.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/synced_sessions_bridge.h"
@interface RecentTabsPanelController ()<SyncedSessionsObserver,
RecentTabsTableViewControllerDelegate> {
std::unique_ptr<synced_sessions::SyncedSessionsObserverBridge>
_syncedSessionsObserver;
std::unique_ptr<recent_tabs::ClosedTabsObserverBridge> _closedTabsObserver;
SessionsSyncUserState _userState;
base::scoped_nsobject<RecentTabsTableViewController> _tableViewController;
ios::ChromeBrowserState* _browserState; // Weak.
}
// Return the user's current sign-in and chrome-sync state.
- (SessionsSyncUserState)userSignedInState;
// Utility functions for -userSignedInState so these can be mocked out
// easily for unit tests.
- (BOOL)isSignedIn;
- (BOOL)isSyncTabsEnabled;
- (BOOL)hasForeignSessions;
- (BOOL)isSyncCompleted;
// Reload the panel.
- (void)refreshSessionsView;
// Force a contact to the sync server to reload remote sessions.
- (void)reloadSessionsData;
@end
// The controller for RecentTabs panel that is added to the NewTabPage
// Instantiate a UITableView and a UITableViewController, and notifies the
// UITableViewController of any signed in state change.
@implementation RecentTabsPanelController
// Property declared in NewTabPagePanelProtocol.
@synthesize delegate = _delegate;
- (instancetype)init {
NOTREACHED();
return nil;
}
- (instancetype)initWithNibNamed:(NSString*)nibName {
NOTREACHED();
return nil;
}
- (instancetype)initWithLoader:(id<UrlLoader>)loader
browserState:(ios::ChromeBrowserState*)browserState {
return [self initWithController:[[[RecentTabsTableViewController alloc]
initWithBrowserState:browserState
loader:loader] autorelease]
browserState:browserState];
}
- (instancetype)initWithController:(RecentTabsTableViewController*)controller
browserState:(ios::ChromeBrowserState*)browserState {
self = [super init];
if (self) {
DCHECK(controller);
DCHECK(browserState);
_browserState = browserState;
_tableViewController.reset([controller retain]);
[_tableViewController setDelegate:self];
[self initObservers];
[self reloadSessions];
}
return self;
}
- (void)dealloc {
[_tableViewController setDelegate:nil];
[self deallocObservers];
[super dealloc];
}
- (void)initObservers {
if (!_syncedSessionsObserver) {
_syncedSessionsObserver.reset(
new synced_sessions::SyncedSessionsObserverBridge(self, _browserState));
}
if (!_closedTabsObserver) {
_closedTabsObserver.reset(new recent_tabs::ClosedTabsObserverBridge(self));
sessions::TabRestoreService* restoreService =
IOSChromeTabRestoreServiceFactory::GetForBrowserState(_browserState);
if (restoreService)
restoreService->AddObserver(_closedTabsObserver.get());
[_tableViewController setTabRestoreService:restoreService];
}
}
- (void)deallocObservers {
_syncedSessionsObserver.reset();
if (_closedTabsObserver) {
sessions::TabRestoreService* restoreService =
IOSChromeTabRestoreServiceFactory::GetForBrowserState(_browserState);
if (restoreService) {
restoreService->RemoveObserver(_closedTabsObserver.get());
}
_closedTabsObserver.reset();
}
}
#pragma mark - Exposed to the SyncedSessionsObserver
- (void)reloadSessions {
[self reloadSessionsData];
[self refreshSessionsView];
}
- (void)onSyncStateChanged {
[self refreshSessionsView];
}
#pragma mark - Exposed to the ClosedTabsObserverBridge
- (void)reloadClosedTabsList {
sessions::TabRestoreService* restoreService =
IOSChromeTabRestoreServiceFactory::GetForBrowserState(_browserState);
restoreService->LoadTabsFromLastSession();
[_tableViewController refreshRecentlyClosedTabs];
}
- (void)tabRestoreServiceDestroyed {
[_tableViewController setTabRestoreService:nullptr];
}
#pragma mark - NewTabPagePanelProtocol
- (void)dismissModals {
[_tableViewController dismissModals];
}
- (void)dismissKeyboard {
}
- (void)reload {
[self reloadSessions];
}
- (void)wasShown {
[[_tableViewController tableView] reloadData];
[self initObservers];
}
- (void)wasHidden {
[self deallocObservers];
}
- (void)setScrollsToTop:(BOOL)enabled {
[_tableViewController setScrollsToTop:enabled];
}
- (CGFloat)alphaForBottomShadow {
UITableView* tableView = [_tableViewController tableView];
CGFloat contentHeight = tableView.contentSize.height;
CGFloat scrollViewHeight = tableView.frame.size.height;
CGFloat offsetY = tableView.contentOffset.y;
CGFloat pixelsBelowFrame = contentHeight - offsetY - scrollViewHeight;
CGFloat alpha = pixelsBelowFrame / kNewTabPageDistanceToFadeShadow;
alpha = MIN(MAX(alpha, 0), 1);
return alpha;
}
- (UIView*)view {
return [_tableViewController view];
}
#pragma mark
#pragma mark - Private
- (BOOL)isSignedIn {
SigninManager* signin_manager =
ios::SigninManagerFactory::GetForBrowserState(_browserState);
return signin_manager->IsAuthenticated();
}
- (BOOL)isSyncTabsEnabled {
DCHECK([self isSignedIn]);
SyncSetupService* service =
SyncSetupServiceFactory::GetForBrowserState(_browserState);
return !service->UserActionIsRequiredToHaveSyncWork();
}
// Returns whether this profile has any foreign sessions to sync.
- (SessionsSyncUserState)userSignedInState {
if (![self isSignedIn])
return SessionsSyncUserState::USER_SIGNED_OUT;
if (![self isSyncTabsEnabled])
return SessionsSyncUserState::USER_SIGNED_IN_SYNC_OFF;
if ([self hasForeignSessions])
return SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS;
if (![self isSyncCompleted])
return SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS;
return SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_NO_SESSIONS;
}
- (BOOL)hasForeignSessions {
browser_sync::ProfileSyncService* service =
IOSChromeProfileSyncServiceFactory::GetForBrowserState(_browserState);
DCHECK(service);
sync_sessions::OpenTabsUIDelegate* openTabs =
service->GetOpenTabsUIDelegate();
std::vector<const sync_sessions::SyncedSession*> sessions;
return openTabs ? openTabs->GetAllForeignSessions(&sessions) : NO;
}
- (BOOL)isSyncCompleted {
return _syncedSessionsObserver->IsFirstSyncCycleCompleted();
}
- (void)reloadSessionsData {
DVLOG(1) << "Triggering sync refresh for sessions datatype.";
const syncer::ModelTypeSet types(syncer::SESSIONS);
// Requests a sync refresh of the sessions for the current profile.
IOSChromeProfileSyncServiceFactory::GetForBrowserState(_browserState)
->TriggerRefresh(types);
}
- (void)refreshSessionsView {
[_tableViewController refreshUserState:[self userSignedInState]];
}
#pragma mark - RecentTabsTableViewControllerDelegate
- (void)recentTabsTableViewContentMoved:(UITableView*)tableView {
[self.delegate updateNtpBarShadowForPanelController:self];
}
@end
// Copyright 2016 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 <EarlGrey/EarlGrey.h>
#import <XCTest/XCTest.h>
#import <map>
#import <string>
#include "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/ui/tools_menu/tools_menu_view_controller.h"
#include "ios/chrome/browser/ui/ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/app/tab_test_util.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/web/public/test/http_server.h"
#include "ios/web/public/test/http_server_util.h"
namespace {
const char kURLOfTestPage[] = "http://testPage";
std::string const kHTMLOfTestPage =
"<head><title>TestPageTitle</title></head><body>hello</body>";
NSString* const kTitleOfTestPage = @"TestPageTitle";
// Makes sure at least one tab is opened and opens the recent tab panel.
void OpenRecentTabsPanel() {
// At least one tab is needed to be able to open the recent tabs panel.
if (chrome_test_util::GetMainTabCount() == 0)
chrome_test_util::OpenNewTab();
[ChromeEarlGreyUI openToolsMenu];
id<GREYMatcher> open_recent_tabs_button_matcher =
grey_accessibilityID(kToolsMenuOtherDevicesId);
[[EarlGrey selectElementWithMatcher:open_recent_tabs_button_matcher]
performAction:grey_tap()];
}
// Closes the recent tabs panel, on iPhone.
void CloseRecentTabsPanelOnIphone() {
DCHECK(!IsIPadIdiom());
id<GREYMatcher> exit_button_matcher = grey_accessibilityID(@"Exit");
[[EarlGrey selectElementWithMatcher:exit_button_matcher]
performAction:grey_tap()];
}
// Returns the matcher for the entry of the page in the recent tabs panel.
id<GREYMatcher> titleOfTestPageMatcher() {
return grey_allOf(
chrome_test_util::staticTextWithAccessibilityLabel(kTitleOfTestPage),
grey_sufficientlyVisible(), nil);
}
// Returns the matcher for the back button.
id<GREYMatcher> backButtonMatcher() {
return chrome_test_util::buttonWithAccessibilityLabelId(IDS_ACCNAME_BACK);
}
// Returns the matcher for the Recently closed label.
id<GREYMatcher> recentlyClosedLabelMatcher() {
return chrome_test_util::staticTextWithAccessibilityLabelId(
IDS_IOS_RECENT_TABS_RECENTLY_CLOSED);
}
} // namespace
// Earl grey integration tests for Recent Tabs Panel Controller.
@interface RecentTabsPanelControllerTestCase : ChromeTestCase
@end
@implementation RecentTabsPanelControllerTestCase
- (void)setUp {
[ChromeEarlGrey clearBrowsingHistory];
[super setUp];
std::map<GURL, std::string> responses;
const GURL testPageURL = web::test::HttpServer::MakeUrl(kURLOfTestPage);
responses[testPageURL] = kHTMLOfTestPage;
web::test::SetUpSimpleHttpServer(responses);
}
- (void)tearDown {
if (IsIPadIdiom()) {
chrome_test_util::OpenNewTab();
NSError* error = nil;
[[EarlGrey selectElementWithMatcher:recentlyClosedLabelMatcher()]
assertWithMatcher:grey_notNil()
error:&error];
// If the Recent Tabs panel is shown, then switch back to the Most Visited
// panel so that tabs opened in other tests will show the Most Visited panel
// instead of the Recent Tabs panel.
if (!error) {
[[EarlGrey selectElementWithMatcher:recentlyClosedLabelMatcher()]
performAction:grey_swipeFastInDirection(kGREYDirectionRight)];
}
chrome_test_util::CloseCurrentTab();
}
}
// Tests that a closed tab appears in the Recent Tabs panel, and that tapping
// the entry in the Recent Tabs panel re-opens the closed tab.
- (void)testClosedTabAppearsInRecentTabsPanel {
const GURL testPageURL = web::test::HttpServer::MakeUrl(kURLOfTestPage);
// Open the test page in a new tab.
[ChromeEarlGrey loadURL:testPageURL];
id<GREYMatcher> webViewMatcher =
chrome_test_util::webViewContainingText("hello");
[[EarlGrey selectElementWithMatcher:webViewMatcher]
assertWithMatcher:grey_notNil()];
// Open the Recent Tabs panel, check that the test page is not
// present.
OpenRecentTabsPanel();
[[EarlGrey selectElementWithMatcher:titleOfTestPageMatcher()]
assertWithMatcher:grey_nil()];
// Get rid of the Recent Tabs Panel.
if (IsIPadIdiom()) {
// On iPad, the Recent Tabs panel is a new page in the navigation history.
// Go back to the previous page to restore the test page.
[[EarlGrey selectElementWithMatcher:backButtonMatcher()]
performAction:grey_tap()];
[ChromeEarlGrey waitForPageToFinishLoading];
} else {
// On iPhone, the Recent Tabs panel is shown in a modal view.
// Close that modal.
CloseRecentTabsPanelOnIphone();
// Wait until the recent tabs panel is dismissed.
[[GREYUIThreadExecutor sharedInstance] drainUntilIdle];
}
// Close the tab containing the test page.
chrome_test_util::CloseCurrentTab();
// Open the Recent Tabs panel and check that the test page is present.
OpenRecentTabsPanel();
[[EarlGrey selectElementWithMatcher:titleOfTestPageMatcher()]
assertWithMatcher:grey_notNil()];
// Tap on the entry for the test page in the Recent Tabs panel and check that
// a tab containing the test page was opened.
[[EarlGrey selectElementWithMatcher:titleOfTestPageMatcher()]
performAction:grey_tap()];
[[EarlGrey selectElementWithMatcher:chrome_test_util::omnibox()]
assertWithMatcher:chrome_test_util::omniboxText(
testPageURL.GetContent())];
}
@end
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_PANEL_VIEW_CONTROLLER_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_PANEL_VIEW_CONTROLLER_H_
#import <UIKit/UIKit.h>
namespace ios {
class ChromeBrowserState;
}
@protocol UrlLoader;
// UIViewController wrapper for RecentTabsPanelController for modal display.
@interface RecentTabsPanelViewController : UIViewController
- (instancetype)initWithLoader:(id<UrlLoader>)loader
browserState:(ios::ChromeBrowserState*)browserState
NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString*)nibNameOrNil
bundle:(NSBundle*)nibBundleOrNil NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE;
+ (UIViewController*)controllerToPresentForBrowserState:
(ios::ChromeBrowserState*)browserState
loader:(id<UrlLoader>)loader;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_PANEL_VIEW_CONTROLLER_H_
// Copyright 2014 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/ntp/recent_tabs/recent_tabs_panel_view_controller.h"
#import "base/mac/scoped_nsobject.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_panel_controller.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/views/panel_bar_view.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_theme_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/resource/resource_bundle.h"
// A UIViewController that forces the status bar to be visible.
@interface RecentTabsWrapperViewController : UIViewController
@end
@implementation RecentTabsWrapperViewController
- (BOOL)prefersStatusBarHidden {
return NO;
}
@end
@implementation RecentTabsPanelViewController {
base::scoped_nsobject<RecentTabsPanelController> _recentTabsController;
base::scoped_nsobject<PanelBarView> _panelBarView;
}
+ (UIViewController*)controllerToPresentForBrowserState:
(ios::ChromeBrowserState*)browserState
loader:(id<UrlLoader>)loader {
UIViewController* controller =
[[[RecentTabsWrapperViewController alloc] init] autorelease];
RecentTabsPanelViewController* rtpvc = [[[RecentTabsPanelViewController alloc]
initWithLoader:loader
browserState:browserState] autorelease];
[controller addChildViewController:rtpvc];
PanelBarView* panelBarView = [[[PanelBarView alloc] init] autorelease];
rtpvc->_panelBarView.reset([panelBarView retain]);
[panelBarView setCloseTarget:rtpvc action:@selector(didFinish)];
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
gfx::Image shadowImage = rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW);
base::scoped_nsobject<UIImageView> shadow(
[[UIImageView alloc] initWithImage:shadowImage.ToUIImage()]);
[panelBarView setTranslatesAutoresizingMaskIntoConstraints:NO];
[rtpvc.view setTranslatesAutoresizingMaskIntoConstraints:NO];
[shadow setTranslatesAutoresizingMaskIntoConstraints:NO];
[controller.view addSubview:panelBarView];
[controller.view addSubview:rtpvc.view];
[controller.view addSubview:shadow];
NSDictionary* viewsDictionary =
@{ @"bar" : panelBarView,
@"table" : rtpvc.view,
@"shadow" : shadow };
// clang-format off
NSArray* constraints = @[
@"V:|-0-[bar]-0-[table]-0-|",
@"V:[bar]-0-[shadow]",
@"H:|-0-[bar]-0-|",
@"H:|-0-[table]-0-|",
@"H:|-0-[shadow]-0-|"
];
// clang-format on
ApplyVisualConstraints(constraints, viewsDictionary, controller.view);
return controller;
}
- (void)dealloc {
[_recentTabsController dismissKeyboard];
[_recentTabsController dismissModals];
[super dealloc];
}
- (instancetype)initWithLoader:(id<UrlLoader>)loader
browserState:(ios::ChromeBrowserState*)browserState {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_recentTabsController.reset([[RecentTabsPanelController alloc]
initWithLoader:loader
browserState:browserState]);
if ([self respondsToSelector:@selector(edgesForExtendedLayout)])
self.edgesForExtendedLayout = UIRectEdgeNone;
}
return self;
}
- (instancetype)initWithNibName:(NSString*)nibNameOrNil
bundle:(NSBundle*)nibBundleOrNil {
NOTREACHED();
return nil;
}
- (instancetype)initWithCoder:(NSCoder*)aDecoder {
NOTREACHED();
return nil;
}
- (void)viewDidLoad {
[super viewDidLoad];
CGRect frame = self.view.bounds;
[_recentTabsController view].frame = frame;
[self.view addSubview:[_recentTabsController view]];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[_panelBarView setNeedsUpdateConstraints];
}
- (BOOL)prefersStatusBarHidden {
return NO;
}
#pragma mark Accessibility
- (BOOL)accessibilityPerformEscape {
[self didFinish];
return YES;
}
#pragma mark Actions
- (void)didFinish {
[self dismissViewControllerAnimated:YES
completion:^{
}];
}
@end
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_TABLE_VIEW_CONTROLLER_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_TABLE_VIEW_CONTROLLER_H_
#import "ios/chrome/browser/ui/ntp/new_tab_page_panel_protocol.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_panel_controller.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/sessions_sync_user_state.h"
namespace sessions {
class TabRestoreService;
}
namespace ios {
class ChromeBrowserState;
}
@protocol RecentTabsTableViewControllerDelegate<NSObject>
// Tells the delegate when the table view content scrolled or changed size.
- (void)recentTabsTableViewContentMoved:(UITableView*)tableView;
@end
// Controls the content of a UITableView.
//
// The UITableView can contain the following different sections:
// A/ Closed tabs section.
// This section lists all the local tabs that were recently closed.
// A*/ Separator section.
// This section contains only a single cell that acts as a separator.
// B/ Other Devices section.
// Depending on the user state, the section will either contain a view
// offering the user to sign in, a view to activate sync, or a view to inform
// the user that they can turn on sync on other devices.
// C/ Session section.
// This section shows the sessions from other devices.
//
// Section A is always present, followed by section A*.
// Depending on the user sync state, either section B or section C will be
// presented.
@interface RecentTabsTableViewController
: UITableViewController<UIGestureRecognizerDelegate>
@property(nonatomic, assign) id<RecentTabsTableViewControllerDelegate>
delegate; // weak
// Designated initializer. The controller opens link with |loader|.
// |browserState|
// and |loader| must not be nil.
- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
loader:(id<UrlLoader>)loader;
// Refreshes the table view to match the current sync state.
- (void)refreshUserState:(SessionsSyncUserState)state;
// Refreshes the recently closed tab section.
- (void)refreshRecentlyClosedTabs;
// Sets the service used to populate the closed tab section. Can be used to nil
// the service in case it is not available anymore.
- (void)setTabRestoreService:(sessions::TabRestoreService*)tabRestoreService;
// Sets whether scroll to top is enabled.
- (void)setScrollsToTop:(BOOL)enabled;
// Dismisses any outstanding modal user interface elements.
- (void)dismissModals;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_RECENT_TABS_TABLE_VIEW_CONTROLLER_H_
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SESSIONS_SYNC_USER_STATE_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SESSIONS_SYNC_USER_STATE_H_
// States listing the user's signed-in and sync status.
enum class SessionsSyncUserState {
USER_SIGNED_OUT,
USER_SIGNED_IN_SYNC_OFF,
USER_SIGNED_IN_SYNC_IN_PROGRESS,
USER_SIGNED_IN_SYNC_ON_NO_SESSIONS,
USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS,
};
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SESSIONS_SYNC_USER_STATE_H_
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SYNCED_SESSIONS_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SYNCED_SESSIONS_H_
#include <memory>
#include <string>
#include <vector>
#include "base/macros.h"
#include "base/strings/string16.h"
#include "base/time/time.h"
#include "components/sessions/core/session_id.h"
#include "components/sync_sessions/synced_session.h"
#include "url/gurl.h"
namespace syncer {
class SyncService;
}
namespace sync_sessions {
class OpenTabsUIDelegate;
}
namespace synced_sessions {
// Data holder that contains the data of the distant tabs to show in the UI.
struct DistantTab {
DistantTab();
// Uniquely identifies the distant session this DistantTab belongs to.
std::string session_tag;
// Uniquely identifies this tab in its distant session.
SessionID::id_type tab_id;
// The title of the page shown in this DistantTab.
base::string16 title;
// The url shown in this DistantTab.
GURL virtual_url;
// Returns a hash the fields |virtual_url| and |title|.
// By design, two tabs in the same distant session can have the same
// |hashOfUserVisibleProperties|.
size_t hashOfUserVisibleProperties();
DISALLOW_COPY_AND_ASSIGN(DistantTab);
};
// Data holder that contains the data of the distant sessions and their tabs to
// show in the UI.
class DistantSession {
public:
DistantSession();
// Initialize with the session tagged with |tag| and obtained with
// |sync_service|. |sync_service| must not be null.
DistantSession(syncer::SyncService* sync_service, const std::string& tag);
~DistantSession();
void InitWithSyncedSession(const sync_sessions::SyncedSession* synced_session,
sync_sessions::OpenTabsUIDelegate* open_tabs);
std::string tag;
std::string name;
base::Time modified_time;
sync_sessions::SyncedSession::DeviceType device_type;
std::vector<std::unique_ptr<DistantTab>> tabs;
DISALLOW_COPY_AND_ASSIGN(DistantSession);
};
// Class containing distant sessions.
class SyncedSessions {
public:
// Initialize with no distant sessions.
SyncedSessions();
// Initialize with all the distant sessions obtained from |sync_service|.
// |sync_service| must not be null.
explicit SyncedSessions(syncer::SyncService* sync_service);
SyncedSessions(syncer::SyncService* sync_service, const std::string& tag);
~SyncedSessions();
DistantSession const* GetSession(size_t index) const;
DistantSession const* GetSessionWithTag(const std::string& tag) const;
size_t GetSessionCount() const;
void EraseSession(size_t index);
// Used by tests only.
void AddDistantSessionForTest(
std::unique_ptr<const DistantSession> distant_session);
private:
std::vector<std::unique_ptr<const DistantSession>> sessions_;
DISALLOW_COPY_AND_ASSIGN(SyncedSessions);
};
} // namespace synced_sessions
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SYNCED_SESSIONS_H_
// Copyright 2014 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.
#include "ios/chrome/browser/ui/ntp/recent_tabs/synced_sessions.h"
#include <functional>
#include <memory>
#include "base/logging.h"
#include "base/mac/scoped_nsobject.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/field_trial.h"
#include "base/strings/utf_string_conversions.h"
#include "components/sync/driver/sync_service.h"
#include "components/sync_sessions/open_tabs_ui_delegate.h"
#include "ios/chrome/browser/sync/sync_setup_service.h"
namespace {
// Comparator function for sorting the sessions_ vector so that the most
// recently used sessions are at the beginning.
bool SortSessionsByTime(
std::unique_ptr<const synced_sessions::DistantSession>& a,
std::unique_ptr<const synced_sessions::DistantSession>& b) {
return a->modified_time > b->modified_time;
}
// Helper to extract the relevant content from a SessionTab and add it to a
// DistantSession.
void AddTabToDistantSession(const sessions::SessionTab& session_tab,
const std::string& session_tag,
synced_sessions::DistantSession* distant_session) {
if (session_tab.navigations.size() > 0) {
distant_session->tabs.push_back(
base::MakeUnique<synced_sessions::DistantTab>());
synced_sessions::DistantTab& distant_tab = *distant_session->tabs.back();
distant_tab.session_tag = session_tag;
distant_tab.tab_id = session_tab.tab_id.id();
int index = session_tab.current_navigation_index;
if (index < 0)
index = 0;
if (index > (int)session_tab.navigations.size() - 1)
index = session_tab.navigations.size() - 1;
const sessions::SerializedNavigationEntry* navigation =
&session_tab.navigations[index];
distant_tab.title = navigation->title();
distant_tab.virtual_url = navigation->virtual_url();
if (distant_tab.title.empty()) {
std::string url = navigation->virtual_url().spec();
distant_tab.title = base::UTF8ToUTF16(url);
}
}
}
} // namespace
namespace synced_sessions {
DistantTab::DistantTab() {}
size_t DistantTab::hashOfUserVisibleProperties() {
std::stringstream ss;
ss << title << std::endl << virtual_url.spec();
return std::hash<std::string>()(ss.str());
}
DistantSession::DistantSession() {}
DistantSession::DistantSession(syncer::SyncService* sync_service,
const std::string& tag) {
if (sync_service->CanSyncStart() && sync_service->GetOpenTabsUIDelegate()) {
sync_sessions::OpenTabsUIDelegate* open_tabs =
sync_service->GetOpenTabsUIDelegate();
std::vector<const sync_sessions::SyncedSession*> sessions;
open_tabs->GetAllForeignSessions(&sessions);
for (const auto& session : sessions) {
if (tag == session->session_tag) {
this->InitWithSyncedSession(session, open_tabs);
}
}
}
}
DistantSession::~DistantSession() {}
void DistantSession::InitWithSyncedSession(
const sync_sessions::SyncedSession* synced_session,
sync_sessions::OpenTabsUIDelegate* open_tabs) {
tag = synced_session->session_tag;
name = synced_session->session_name;
modified_time = synced_session->modified_time;
device_type = synced_session->device_type;
const std::string group_name =
base::FieldTrialList::FindFullName("TabSyncByRecency");
if (group_name == "Enabled") {
// Order tabs by recency.
std::vector<const sessions::SessionTab*> session_tabs;
open_tabs->GetForeignSessionTabs(synced_session->session_tag,
&session_tabs);
for (const auto& session_tab : session_tabs) {
AddTabToDistantSession(*session_tab, synced_session->session_tag, this);
}
} else {
// Order tabs by their visual position within window.
for (const auto& kv : synced_session->windows) {
for (const auto& session_tab : kv.second->tabs) {
AddTabToDistantSession(*session_tab, synced_session->session_tag, this);
}
}
}
}
SyncedSessions::SyncedSessions() {}
SyncedSessions::SyncedSessions(syncer::SyncService* sync_service) {
DCHECK(sync_service);
// Reload Sync open tab sesions.
if (sync_service->CanSyncStart() && sync_service->GetOpenTabsUIDelegate()) {
sync_sessions::OpenTabsUIDelegate* open_tabs =
sync_service->GetOpenTabsUIDelegate();
// Iterating through all remote sessions, then all remote windows, then all
// remote tabs to retrieve the tabs to display to the user. This will
// flatten all of those data into a sequential vector of tabs.
std::vector<const sync_sessions::SyncedSession*> sessions;
open_tabs->GetAllForeignSessions(&sessions);
for (const auto& session : sessions) {
std::unique_ptr<DistantSession> distant_session(new DistantSession());
distant_session->InitWithSyncedSession(session, open_tabs);
// Don't display sessions with no tabs.
if (distant_session->tabs.size() > 0)
sessions_.push_back(std::move(distant_session));
}
}
std::sort(sessions_.begin(), sessions_.end(), SortSessionsByTime);
}
SyncedSessions::~SyncedSessions() {}
// Returns the session at index |index|.
DistantSession const* SyncedSessions::GetSession(size_t index) const {
DCHECK_LE(index, GetSessionCount());
return sessions_[index].get();
}
const DistantSession* SyncedSessions::GetSessionWithTag(
const std::string& tag) const {
for (auto const& distant_session : sessions_) {
if (distant_session->tag == tag) {
return distant_session.get();
}
}
return nullptr;
}
// Returns the number of distant sessions.
size_t SyncedSessions::GetSessionCount() const {
return sessions_.size();
}
// Deletes the session at index |index|.
void SyncedSessions::EraseSession(size_t index) {
DCHECK_LE(index, GetSessionCount());
sessions_.erase(sessions_.begin() + index);
}
void SyncedSessions::AddDistantSessionForTest(
std::unique_ptr<const DistantSession> distant_session) {
sessions_.push_back(std::move(distant_session));
}
} // synced_sessions namespace
// Copyright 2015 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SYNCED_SESSIONS_BRIDGE_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SYNCED_SESSIONS_BRIDGE_H_
#import <UIKit/UIKit.h>
#import "base/ios/weak_nsobject.h"
#include "components/signin/core/browser/signin_manager_base.h"
#include "components/sync/driver/sync_service_observer.h"
#import "ios/chrome/browser/sync/sync_observer_bridge.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/synced_sessions_bridge.h"
namespace ios {
class ChromeBrowserState;
}
class SigninManager;
@class RecentTabsPanelController;
@protocol SyncedSessionsObserver<SyncObserverModelBridge>
- (void)reloadSessions;
@end
namespace synced_sessions {
// Bridge class that will notify the panel when the remote sessions content
// change.
class SyncedSessionsObserverBridge : public SyncObserverBridge,
public SigninManagerBase::Observer {
public:
SyncedSessionsObserverBridge(id<SyncedSessionsObserver> owner,
ios::ChromeBrowserState* browserState);
~SyncedSessionsObserverBridge() override;
// SyncObserverBridge implementation.
void OnStateChanged() override;
void OnSyncCycleCompleted() override;
void OnSyncConfigurationCompleted() override;
void OnForeignSessionUpdated() override;
// SigninManagerBase::Observer implementation.
void GoogleSignedOut(const std::string& account_id,
const std::string& username) override;
// Returns true if the first sync cycle that contains session information is
// completed. Returns false otherwise.
bool IsFirstSyncCycleCompleted();
private:
base::WeakNSProtocol<id<SyncedSessionsObserver>> owner_;
SigninManager* signin_manager_;
syncer::SyncService* sync_service_;
ScopedObserver<SigninManagerBase, SigninManagerBase::Observer>
signin_manager_observer_;
// Stores whether the first sync cycle that contains session information is
// completed.
bool first_sync_cycle_is_completed_;
};
} // namespace synced_sessions
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_SYNCED_SESSIONS_BRIDGE_H_
// Copyright 2015 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/ntp/recent_tabs/synced_sessions_bridge.h"
#include "components/browser_sync/profile_sync_service.h"
#include "components/signin/core/browser/signin_manager.h"
#include "components/sync/driver/sync_service.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/signin/signin_manager_factory.h"
#include "ios/chrome/browser/sync/ios_chrome_profile_sync_service_factory.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_table_view_controller.h"
namespace synced_sessions {
#pragma mark - SyncedSessionsObserverBridge
SyncedSessionsObserverBridge::SyncedSessionsObserverBridge(
id<SyncedSessionsObserver> owner,
ios::ChromeBrowserState* browserState)
: SyncObserverBridge(
owner,
IOSChromeProfileSyncServiceFactory::GetForBrowserState(browserState)),
owner_(owner),
signin_manager_(
ios::SigninManagerFactory::GetForBrowserState(browserState)),
sync_service_(
IOSChromeProfileSyncServiceFactory::GetForBrowserState(browserState)),
signin_manager_observer_(this),
first_sync_cycle_is_completed_(false) {
signin_manager_observer_.Add(signin_manager_);
}
SyncedSessionsObserverBridge::~SyncedSessionsObserverBridge() {}
#pragma mark - SyncObserverBridge
void SyncedSessionsObserverBridge::OnStateChanged() {
if (!signin_manager_->IsAuthenticated())
first_sync_cycle_is_completed_ = false;
[owner_ onSyncStateChanged];
}
void SyncedSessionsObserverBridge::OnSyncCycleCompleted() {
if (sync_service_->GetActiveDataTypes().Has(syncer::SESSIONS))
first_sync_cycle_is_completed_ = true;
[owner_ onSyncStateChanged];
}
void SyncedSessionsObserverBridge::OnSyncConfigurationCompleted() {
[owner_ reloadSessions];
}
void SyncedSessionsObserverBridge::OnForeignSessionUpdated() {
[owner_ reloadSessions];
}
bool SyncedSessionsObserverBridge::IsFirstSyncCycleCompleted() {
return first_sync_cycle_is_completed_;
}
#pragma mark - SigninManagerBase::Observer
void SyncedSessionsObserverBridge::GoogleSignedOut(
const std::string& account_id,
const std::string& username) {
first_sync_cycle_is_completed_ = false;
[owner_ reloadSessions];
}
} // namespace synced_sessions
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_DISCLOSURE_VIEW_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_DISCLOSURE_VIEW_H_
#import <UIKit/UIKit.h>
// View indicating whether a table view section is expanded or not.
@interface DisclosureView : UIImageView
// Designated initializer.
- (instancetype)init;
// Sets whether the view indicates that the section is collapsed or not, with an
// animation or not.
- (void)setTransformWhenCollapsed:(BOOL)collapsed animated:(BOOL)animated;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_DISCLOSURE_VIEW_H_
// Copyright 2014 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.
#include "ios/chrome/browser/ui/ntp/recent_tabs/views/disclosure_view.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Animation duration for rotating the disclosure icon.
const NSTimeInterval kDisclosureIconRotateDuration = 0.25;
// Angles of the closure icon.
// The rotation animation privileges rotating using the smallest angle. Setting
// |kCollapsedIconAngle| to a value slightly less then 0 forces the animation to
// always happen in the same half-plane.
const CGFloat kCollapsedIconAngle = -0.00001;
const CGFloat kExpandedIconAngle = M_PI;
} // anonymous namespace
@implementation DisclosureView
- (instancetype)init {
UIImage* arrowImage = [[UIImage imageNamed:@"ntp_opentabs_recent_arrow"]
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
self = [super initWithImage:arrowImage];
return self;
}
- (void)setTransformWhenCollapsed:(BOOL)collapsed animated:(BOOL)animated {
CGFloat angle = collapsed ? kCollapsedIconAngle : kExpandedIconAngle;
if (animated) {
[UIView animateWithDuration:kDisclosureIconRotateDuration
animations:^{
self.transform = CGAffineTransformRotate(
CGAffineTransformIdentity, angle);
}];
} else {
self.transform = CGAffineTransformRotate(CGAffineTransformIdentity, angle);
}
}
@end
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_GENERIC_SECTION_HEADER_VIEW_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_GENERIC_SECTION_HEADER_VIEW_H_
#import <UIKit/UIKit.h>
#import "ios/chrome/browser/ui/ntp/recent_tabs/views/header_of_collapsable_section_protocol.h"
namespace recent_tabs {
enum SectionHeaderType {
RECENTLY_CLOSED_TABS_SECTION_HEADER,
OTHER_DEVICES_SECTION_HEADER
};
} // namespace recent_tabs
// View for the generic section header.
@interface GenericSectionHeaderView : UIView<HeaderOfCollapsableSectionProtocol>
// Designated initializer.
- (instancetype)initWithType:(recent_tabs::SectionHeaderType)type
sectionIsCollapsed:(BOOL)collapsed;
// Returns the desired height when included in a UITableViewCell.
+ (CGFloat)desiredHeightInUITableViewCell;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_GENERIC_SECTION_HEADER_VIEW_H_
// Copyright 2014 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.
#include "ios/chrome/browser/ui/ntp/recent_tabs/views/generic_section_header_view.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "ios/chrome/browser/ui/ntp/recent_tabs/synced_sessions.h"
#include "ios/chrome/browser/ui/ntp/recent_tabs/views/disclosure_view.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/views/views_utils.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoFontLoader.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/l10n/time_format.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Desired height of the view.
const CGFloat kDesiredHeight = 48;
} // namespace
@interface GenericSectionHeaderView () {
DisclosureView* _disclosureView;
UIImageView* _icon;
UILabel* _label;
}
@end
@implementation GenericSectionHeaderView
- (instancetype)initWithFrame:(CGRect)aRect {
NOTREACHED();
return nil;
}
- (instancetype)initWithType:(recent_tabs::SectionHeaderType)type
sectionIsCollapsed:(BOOL)collapsed {
self = [super initWithFrame:CGRectZero];
if (self) {
_icon = [[UIImageView alloc] initWithImage:nil];
[_icon setTranslatesAutoresizingMaskIntoConstraints:NO];
_label = [[UILabel alloc] initWithFrame:CGRectZero];
[_label setTranslatesAutoresizingMaskIntoConstraints:NO];
[_label
setFont:[[MDFRobotoFontLoader sharedInstance] regularFontOfSize:16]];
[_label setTextAlignment:NSTextAlignmentNatural];
[_label setBackgroundColor:[UIColor whiteColor]];
NSString* text = nil;
NSString* imageName = nil;
switch (type) {
case recent_tabs::RECENTLY_CLOSED_TABS_SECTION_HEADER:
text = l10n_util::GetNSString(IDS_IOS_RECENT_TABS_RECENTLY_CLOSED);
imageName = @"ntp_recently_closed";
break;
case recent_tabs::OTHER_DEVICES_SECTION_HEADER:
text = l10n_util::GetNSString(IDS_IOS_RECENT_TABS_OTHER_DEVICES);
imageName = @"ntp_opentabs_laptop";
break;
}
DCHECK(text);
DCHECK(imageName);
[_label setText:text];
[_icon setImage:
[[UIImage imageNamed:imageName]
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
self.isAccessibilityElement = YES;
self.accessibilityLabel = [_label accessibilityLabel];
self.accessibilityTraits |=
UIAccessibilityTraitButton | UIAccessibilityTraitHeader;
_disclosureView = [[DisclosureView alloc] init];
[_disclosureView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:_icon];
[self addSubview:_label];
[self addSubview:_disclosureView];
NSDictionary* viewsDictionary = @{
@"icon" : _icon,
@"label" : _label,
@"disclosureView" : _disclosureView,
};
NSArray* constraints = @[
@"H:|-16-[icon]-16-[label]-(>=16)-[disclosureView]-16-|",
@"V:|-12-[label]-12-|"
];
ApplyVisualConstraintsWithOptions(constraints, viewsDictionary,
LayoutOptionForRTLSupport(), self);
[self addConstraint:[NSLayoutConstraint
constraintWithItem:_disclosureView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
[self addConstraint:[NSLayoutConstraint
constraintWithItem:_icon
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:_label
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
[self setSectionIsCollapsed:collapsed animated:NO];
}
return self;
}
+ (CGFloat)desiredHeightInUITableViewCell {
return kDesiredHeight;
}
#pragma mark - HeaderOfCollapsableSectionProtocol
- (void)setSectionIsCollapsed:(BOOL)collapsed animated:(BOOL)animated {
[_disclosureView setTransformWhenCollapsed:collapsed animated:animated];
UIColor* tintColor = (collapsed ? recent_tabs::GetIconColorGray()
: recent_tabs::GetIconColorBlue());
[self setTintColor:tintColor];
UIColor* textColor = (collapsed ? recent_tabs::GetTextColorGray()
: recent_tabs::GetTextColorBlue());
[_label setTextColor:textColor];
self.accessibilityHint =
collapsed ? l10n_util::GetNSString(
IDS_IOS_RECENT_TABS_DISCLOSURE_VIEW_COLLAPSED_HINT)
: l10n_util::GetNSString(
IDS_IOS_RECENT_TABS_DISCLOSURE_VIEW_EXPANDED_HINT);
}
@end
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_HEADER_OF_COLLAPSABLE_SECTION_PROTOCOL_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_HEADER_OF_COLLAPSABLE_SECTION_PROTOCOL_H_
// Implemented by views that are headers of collapsable sections.
@protocol HeaderOfCollapsableSectionProtocol
// Sets whether the section is collapsed with or without animation.
- (void)setSectionIsCollapsed:(BOOL)collapsed animated:(BOOL)animated;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_HEADER_OF_COLLAPSABLE_SECTION_PROTOCOL_H_
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_PANEL_BAR_VIEW_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_PANEL_BAR_VIEW_H_
#import <UIKit/UIKit.h>
// View for the bar located at the top of the Recent Tabs panel.
@interface PanelBarView : UIView
// Designated initializer.
- (instancetype)init;
// Sets the target/action of the close button.
- (void)setCloseTarget:(id)target action:(SEL)action;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_PANEL_BAR_VIEW_H_
// Copyright 2014 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.
#include "ios/chrome/browser/ui/ntp/recent_tabs/views/panel_bar_view.h"
#import "ios/chrome/browser/ui/ntp/recent_tabs/views/views_utils.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h"
#import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoFontLoader.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const int kBackgroundColor = 0xf2f2f2;
const CGFloat kFontSize = 20;
} // namespace
@interface PanelBarView () {
UIButton* _closeButton;
NSLayoutConstraint* _statusBarSpacerConstraint;
}
// Whether the panel view extends throughout the whole screen. For example,
// when presented fullscreen, the panel bar extends to the borders of the app
// and the function returns YES.
// When presented modally as on iPad and iPhone 6 Plus landscape, it returns NO.
- (BOOL)coversFullAppWidth;
@end
@implementation PanelBarView
- (instancetype)init {
self = [super initWithFrame:CGRectZero];
if (self) {
[self setBackgroundColor:UIColorFromRGB(kBackgroundColor)];
// Create and add the bar's title.
UILabel* title = [[UILabel alloc] initWithFrame:CGRectZero];
[title setTranslatesAutoresizingMaskIntoConstraints:NO];
[title setFont:[[MDFRobotoFontLoader sharedInstance]
mediumFontOfSize:kFontSize]];
[title setTextColor:recent_tabs::GetTextColorGray()];
[title setTextAlignment:NSTextAlignmentNatural];
[title setText:l10n_util::GetNSString(IDS_IOS_NEW_TAB_RECENT_TABS)];
[title setBackgroundColor:UIColorFromRGB(kBackgroundColor)];
[self addSubview:title];
// Create and add the bar's close button.
_closeButton = [[UIButton alloc] initWithFrame:CGRectZero];
[_closeButton setTranslatesAutoresizingMaskIntoConstraints:NO];
[_closeButton
setTitle:l10n_util::GetNSString(IDS_IOS_NAVIGATION_BAR_DONE_BUTTON)
.uppercaseString
forState:UIControlStateNormal];
[_closeButton setTitleColor:recent_tabs::GetTextColorGray()
forState:UIControlStateNormal];
[[_closeButton titleLabel] setFont:[MDCTypography buttonFont]];
[_closeButton setAccessibilityIdentifier:@"Exit"];
[self addSubview:_closeButton];
// Create and add the view that adds vertical padding that matches the
// status bar's height.
UIView* statusBarSpacer = [[UIView alloc] initWithFrame:CGRectZero];
[statusBarSpacer setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:statusBarSpacer];
// Add the constraints on all the subviews.
NSDictionary* viewsDictionary = @{
@"title" : title,
@"closeButton" : _closeButton,
@"statusBar" : statusBarSpacer,
};
NSArray* constraints = @[
@"V:|-0-[statusBar]-14-[closeButton]-13-|",
@"H:|-16-[title]-(>=0)-[closeButton]-16-|",
];
ApplyVisualConstraintsWithOptions(constraints, viewsDictionary,
LayoutOptionForRTLSupport(), self);
AddSameCenterYConstraint(self, title, _closeButton);
_statusBarSpacerConstraint =
[NSLayoutConstraint constraintWithItem:statusBarSpacer
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:0];
[self addConstraint:_statusBarSpacerConstraint];
}
return self;
}
- (void)updateConstraints {
UIInterfaceOrientation orientation =
[[UIApplication sharedApplication] statusBarOrientation];
// On Plus phones in landscape, the modal is not fullscreen. The panel bar
// doesn't need to take the status bar into account.
BOOL takeStatusBarIntoAccount = [self coversFullAppWidth] ||
UIInterfaceOrientationIsPortrait(orientation);
if (takeStatusBarIntoAccount) {
CGFloat statusBarHeight = StatusBarHeight();
[_statusBarSpacerConstraint setConstant:statusBarHeight];
} else {
[_statusBarSpacerConstraint setConstant:0];
}
[super updateConstraints];
}
- (void)setCloseTarget:(id)target action:(SEL)action {
[_closeButton addTarget:target
action:action
forControlEvents:UIControlEventTouchUpInside];
}
- (void)layoutSubviews {
[self setNeedsUpdateConstraints];
[super layoutSubviews];
}
- (BOOL)coversFullAppWidth {
return self.traitCollection.horizontalSizeClass ==
self.window.traitCollection.horizontalSizeClass;
}
@end
// Copyright 2014 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.
#ifndef IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_SESSION_SECTION_HEADER_VIEW_H_
#define IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_SESSION_SECTION_HEADER_VIEW_H_
#import <UIKit/UIKit.h>
#import "ios/chrome/browser/ui/ntp/recent_tabs/views/header_of_collapsable_section_protocol.h"
namespace synced_sessions {
class DistantSession;
} // namespace synced_sessions
// View for the header of the sessions section.
@interface SessionSectionHeaderView : UIView<HeaderOfCollapsableSectionProtocol>
// Designated initializer.
- (instancetype)initWithFrame:(CGRect)aRect sectionIsCollapsed:(BOOL)collapsed;
// Updates view to display information for |distantSession|.
- (void)updateWithSession:
(synced_sessions::DistantSession const*)distantSession;
// Returns the desired height when included in a UITableViewCell.
+ (CGFloat)desiredHeightInUITableViewCell;
@end
#endif // IOS_CHROME_BROWSER_UI_NTP_RECENT_TABS_VIEWS_SESSION_SECTION_HEADER_VIEW_H_
// Copyright 2014 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.
#include "ios/chrome/browser/ui/ntp/recent_tabs/views/session_section_header_view.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "ios/chrome/browser/ui/ntp/recent_tabs/synced_sessions.h"
#include "ios/chrome/browser/ui/ntp/recent_tabs/views/disclosure_view.h"
#include "ios/chrome/browser/ui/ntp/recent_tabs/views/views_utils.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h"
#import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoFontLoader.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/l10n/time_format.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Desired height of the view.
const CGFloat kDesiredHeight = 72;
// The UI displays relative time for up to this number of hours and then
// switches to absolute values.
const int kRelativeTimeMaxHours = 4;
} // namespace
@interface SessionSectionHeaderView () {
UIImageView* _deviceIcon;
UILabel* _nameLabel;
UILabel* _timeLabel;
DisclosureView* _disclosureView;
}
// Returns a relative string (e.g. 15 mins ago) if the time passed in is within
// the last 4 hours. Returns the full formatted time in short style otherwise.
- (NSString*)relativeTimeStringForTime:(base::Time)time;
@end
@implementation SessionSectionHeaderView
- (instancetype)initWithFrame:(CGRect)aRect sectionIsCollapsed:(BOOL)collapsed {
self = [super initWithFrame:aRect];
if (self) {
_deviceIcon = [[UIImageView alloc] initWithImage:nil];
[_deviceIcon setTranslatesAutoresizingMaskIntoConstraints:NO];
_nameLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[_nameLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[_nameLabel setHighlightedTextColor:[_nameLabel textColor]];
[_nameLabel setTextAlignment:NSTextAlignmentNatural];
[_nameLabel
setFont:[[MDFRobotoFontLoader sharedInstance] regularFontOfSize:16]];
[_nameLabel setBackgroundColor:[UIColor whiteColor]];
[_nameLabel
setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:
UILayoutConstraintAxisHorizontal];
_timeLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[_timeLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
[_timeLabel setTextAlignment:NSTextAlignmentNatural];
[_timeLabel setFont:[MDCTypography captionFont]];
[_timeLabel setHighlightedTextColor:[_timeLabel textColor]];
[_timeLabel setBackgroundColor:[UIColor whiteColor]];
_disclosureView = [[DisclosureView alloc] init];
[_disclosureView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self addSubview:_deviceIcon];
[self addSubview:_nameLabel];
[self addSubview:_timeLabel];
[self addSubview:_disclosureView];
NSDictionary* viewsDictionary = @{
@"deviceIcon" : _deviceIcon,
@"nameLabel" : _nameLabel,
@"timeLabel" : _timeLabel,
@"disclosureView" : _disclosureView,
};
NSArray* constraints = @[
@"H:|-16-[deviceIcon]-16-[nameLabel]-(>=16)-[disclosureView]-16-|",
@"V:|-16-[nameLabel]-5-[timeLabel]-16-|",
@"H:[deviceIcon]-16-[timeLabel]",
];
ApplyVisualConstraintsWithOptions(constraints, viewsDictionary,
LayoutOptionForRTLSupport(), self);
[self addConstraint:[NSLayoutConstraint
constraintWithItem:_disclosureView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
[self addConstraint:[NSLayoutConstraint
constraintWithItem:_deviceIcon
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:_nameLabel
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
[self setSectionIsCollapsed:collapsed animated:NO];
}
// TODO(jif): Add timer that refreshes the time label.
return self;
}
- (void)updateWithSession:
(synced_sessions::DistantSession const*)distantSession {
NSString* imageName = nil;
switch (distantSession->device_type) {
case sync_sessions::SyncedSession::TYPE_PHONE:
imageName = @"ntp_opentabs_phone";
break;
case sync_sessions::SyncedSession::TYPE_TABLET:
imageName = @"ntp_opentabs_tablet";
break;
default:
imageName = @"ntp_opentabs_laptop";
break;
}
[_deviceIcon
setImage:[[UIImage imageNamed:imageName]
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
[_nameLabel setText:base::SysUTF8ToNSString(distantSession->name)];
NSDate* lastUsedDate = [NSDate
dateWithTimeIntervalSince1970:distantSession->modified_time.ToTimeT()];
NSString* timeString =
[self relativeTimeStringForTime:distantSession->modified_time];
NSString* dateString =
[NSDateFormatter localizedStringFromDate:lastUsedDate
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterNoStyle];
NSString* timeDateString =
[NSString stringWithFormat:@"%@ %@", timeString, dateString];
[_timeLabel setText:l10n_util::GetNSStringF(
IDS_IOS_OPEN_TABS_LAST_USED,
base::SysNSStringToUTF16(timeDateString))];
}
- (NSString*)relativeTimeStringForTime:(base::Time)time {
base::TimeDelta last_used_delta;
if (base::Time::Now() > time)
last_used_delta = base::Time::Now() - time;
if (last_used_delta.ToInternalValue() < base::Time::kMicrosecondsPerMinute)
return l10n_util::GetNSString(IDS_IOS_OPEN_TABS_RECENTLY_SYNCED);
if (last_used_delta.InHours() < kRelativeTimeMaxHours) {
return SysUTF16ToNSString(
ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_ELAPSED,
ui::TimeFormat::LENGTH_SHORT, last_used_delta));
}
NSDate* date = [NSDate dateWithTimeIntervalSince1970:time.ToTimeT()];
return [NSDateFormatter localizedStringFromDate:date
dateStyle:NSDateFormatterNoStyle
timeStyle:NSDateFormatterShortStyle];
}
+ (CGFloat)desiredHeightInUITableViewCell {
return kDesiredHeight;
}
#pragma mark - HeaderOfCollapsableSectionProtocol
- (void)setSectionIsCollapsed:(BOOL)collapsed animated:(BOOL)animated {
[_disclosureView setTransformWhenCollapsed:collapsed animated:animated];
UIColor* tintColor = (collapsed ? recent_tabs::GetIconColorGray()
: recent_tabs::GetIconColorBlue());
[self setTintColor:tintColor];
UIColor* textColor = (collapsed ? recent_tabs::GetTextColorGray()
: recent_tabs::GetTextColorBlue());
[_nameLabel setTextColor:textColor];
UIColor* subtitleColor = (collapsed ? recent_tabs::GetSubtitleColorGray()
: recent_tabs::GetSubtitleColorBlue());
[_timeLabel setTextColor:subtitleColor];
}
@end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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