Commit 9ac0ba48 authored by gogerald's avatar gogerald Committed by Commit Bot

Reland "[StartSurface] Switch to normal tab model on resume to V2 start surface"

This is a reland of 9b955d94

Original change's description:
> [StartSurface] Switch to normal tab model on resume to V2 start surface
> 
> This CL also solves the scroll issue of the incognito description.
> 
> Screen records:
> Resume:
> https://drive.google.com/file/d/1jPFCAEwTDzI4PO-Ef-HdvRV6KeZoW4fD/view?usp=sharing
> 
> Scroll:
> https://drive.google.com/file/d/1C2lFMBKmdTaipWcHBG9mAYCJNuhoiYK_/view?usp=sharing
> 
> Bug: 1093506
> Change-Id: I5d7365daa530b23f897a19665076cbc32238224f
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2243334
> Commit-Queue: Ganggui Tang <gogerald@chromium.org>
> Auto-Submit: Ganggui Tang <gogerald@chromium.org>
> Reviewed-by: Yusuf Ozuysal <yusufo@chromium.org>
> Reviewed-by: Wei-Yin Chen (陳威尹) <wychen@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#779878}

Bug: 1093506
Change-Id: Ie49546277d347eea6682064a41a859f52b882a7e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2254578
Auto-Submit: Ganggui Tang <gogerald@chromium.org>
Commit-Queue: Yusuf Ozuysal <yusufo@chromium.org>
Reviewed-by: default avatarYusuf Ozuysal <yusufo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#780386}
parent a55ffb35
......@@ -8,6 +8,7 @@ import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_FAKE_S
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO_DESCRIPTION_INITIALIZED;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO_DESCRIPTION_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_SURFACE_BODY_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_TAB_CAROUSEL_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_VOICE_RECOGNITION_BUTTON_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.MORE_TABS_CLICK_LISTENER;
......@@ -748,11 +749,13 @@ class StartSurfaceMediator
mPropertyModel.set(IS_INCOGNITO_DESCRIPTION_INITIALIZED, true);
}
mPropertyModel.set(IS_INCOGNITO_DESCRIPTION_VISIBLE, isVisible);
mPropertyModel.set(IS_SURFACE_BODY_VISIBLE, !isVisible);
if (mSecondaryTasksSurfacePropertyModel != null) {
if (!mSecondaryTasksSurfacePropertyModel.get(IS_INCOGNITO_DESCRIPTION_INITIALIZED)) {
mSecondaryTasksSurfacePropertyModel.set(IS_INCOGNITO_DESCRIPTION_INITIALIZED, true);
}
mSecondaryTasksSurfacePropertyModel.set(IS_INCOGNITO_DESCRIPTION_VISIBLE, isVisible);
mSecondaryTasksSurfacePropertyModel.set(IS_SURFACE_BODY_VISIBLE, !isVisible);
}
}
......
......@@ -38,6 +38,7 @@ import static org.chromium.chrome.test.util.ViewUtils.onViewWaiting;
import static org.chromium.chrome.test.util.ViewUtils.waitForView;
import android.content.Intent;
import android.os.Build;
import android.support.test.InstrumentationRegistry;
import android.view.KeyEvent;
import android.view.ViewGroup;
......@@ -639,6 +640,10 @@ public class StartSurfaceTest {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mActivityTestRule.getActivity().getTabModelSelector().selectModel(true);
});
// TODO(crbug.com/1097001): remove after fixing the default focus issue, which might
// relate to crbug.com/1076274 above since it doesn't exist for the other combinations.
assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
} else {
onView(withId(R.id.incognito_switch)).perform(click());
}
......
......@@ -21,6 +21,7 @@ import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_FAKE_S
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO_DESCRIPTION_INITIALIZED;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO_DESCRIPTION_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_SURFACE_BODY_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_TAB_CAROUSEL_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_VOICE_RECOGNITION_BUTTON_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.MV_TILES_CONTAINER_TOP_MARGIN;
......@@ -1078,16 +1079,19 @@ public class StartSurfaceMediatorUnitTest {
assertThat(mPropertyModel.get(IS_INCOGNITO_DESCRIPTION_INITIALIZED), equalTo(false));
assertThat(mPropertyModel.get(IS_INCOGNITO_DESCRIPTION_VISIBLE), equalTo(false));
assertThat(mPropertyModel.get(IS_SURFACE_BODY_VISIBLE), equalTo(false));
doReturn(0).when(mIncognitoTabModel).getCount();
mTabModelSelectorObserverCaptor.getValue().onTabModelSelected(
mIncognitoTabModel, mNormalTabModel);
assertThat(mPropertyModel.get(IS_INCOGNITO_DESCRIPTION_INITIALIZED), equalTo(true));
assertThat(mPropertyModel.get(IS_INCOGNITO_DESCRIPTION_VISIBLE), equalTo(true));
assertThat(mPropertyModel.get(IS_SURFACE_BODY_VISIBLE), equalTo(false));
mTabModelSelectorObserverCaptor.getValue().onTabModelSelected(
mNormalTabModel, mIncognitoTabModel);
assertThat(mPropertyModel.get(IS_INCOGNITO_DESCRIPTION_INITIALIZED), equalTo(true));
assertThat(mPropertyModel.get(IS_INCOGNITO_DESCRIPTION_VISIBLE), equalTo(false));
assertThat(mPropertyModel.get(IS_SURFACE_BODY_VISIBLE), equalTo(true));
mediator.hideOverview(true);
}
......
......@@ -55,6 +55,7 @@ android_resources("java_resources") {
"java/res/layout/bottom_tab_strip_toolbar.xml",
"java/res/layout/closable_tab_grid_card_item.xml",
"java/res/layout/closable_tab_list_card_item.xml",
"java/res/layout/incognito_description_container_layout.xml",
"java/res/layout/iph_drag_and_drop_dialog_layout.xml",
"java/res/layout/new_tab_tile_card_item.xml",
"java/res/layout/selectable_tab_grid_card_item.xml",
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/incognito_description_layout" />
</androidx.core.widget.NestedScrollView>
......@@ -16,12 +16,6 @@
android:layout_height="wrap_content"
app:elevation="0dp">
<include layout="@layout/fake_search_box_layout"/>
<ViewStub
android:id="@+id/incognito_description_layout_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/incognito_description_layout"
app:layout_scrollFlags="scroll" />
<HorizontalScrollView android:id="@+id/mv_tiles_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
......@@ -78,9 +72,21 @@
android:visibility="gone"
app:layout_scrollFlags="scroll"/>
</com.google.android.material.appbar.AppBarLayout>
<!-- This incognito_description_container_layout_stub and the below tasks_surface_body
should be mutually exclusive, otherwise they will overlap each other. -->
<ViewStub
android:id="@+id/incognito_description_container_layout_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/incognito_description_container_layout"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<!-- Note that this FrameLayout can not be added to NestedScrollView, otherwise the
RecyclerView added to this FrameLayout won't recycle. -->
<FrameLayout
android:id="@+id/tasks_surface_body"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</org.chromium.chrome.browser.tasks.TasksView>
......@@ -14,6 +14,7 @@ import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.INCOGNITO
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.INCOGNITO_COOKIE_CONTROLS_TOGGLE_ENFORCEMENT;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.INCOGNITO_LEARN_MORE_CLICK_LISTENER;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_FAKE_SEARCH_BOX_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_SURFACE_BODY_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_TAB_CAROUSEL_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_VOICE_RECOGNITION_BUTTON_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.VOICE_SEARCH_BUTTON_CLICK_LISTENER;
......@@ -52,6 +53,9 @@ class TasksSurfaceMediator {
// Set Incognito Cookie Controls functionality
mIncognitoCookieControlsManager = incognitoCookieControlsManager;
mModel.set(INCOGNITO_COOKIE_CONTROLS_MANAGER, mIncognitoCookieControlsManager);
// Set the initial state.
mModel.set(IS_SURFACE_BODY_VISIBLE, true);
}
public void initWithNative(FakeboxDelegate fakeboxDelegate) {
......
......@@ -27,6 +27,8 @@ public class TasksSurfaceProperties {
new PropertyModel.WritableBooleanPropertyKey();
public static final PropertyModel.WritableBooleanPropertyKey IS_INCOGNITO_DESCRIPTION_VISIBLE =
new PropertyModel.WritableBooleanPropertyKey();
public static final PropertyModel.WritableBooleanPropertyKey IS_SURFACE_BODY_VISIBLE =
new PropertyModel.WritableBooleanPropertyKey();
public static final PropertyModel.WritableBooleanPropertyKey IS_TAB_CAROUSEL_VISIBLE =
new PropertyModel.WritableBooleanPropertyKey();
public static final PropertyModel
......@@ -74,7 +76,7 @@ public class TasksSurfaceProperties {
new PropertyModel.WritableIntPropertyKey();
public static final PropertyKey[] ALL_KEYS = new PropertyKey[] {IS_FAKE_SEARCH_BOX_VISIBLE,
IS_INCOGNITO, IS_INCOGNITO_DESCRIPTION_INITIALIZED, IS_INCOGNITO_DESCRIPTION_VISIBLE,
IS_TAB_CAROUSEL_VISIBLE, IS_VOICE_RECOGNITION_BUTTON_VISIBLE,
IS_SURFACE_BODY_VISIBLE, IS_TAB_CAROUSEL_VISIBLE, IS_VOICE_RECOGNITION_BUTTON_VISIBLE,
INCOGNITO_COOKIE_CONTROLS_CARD_VISIBILITY,
INCOGNITO_COOKIE_CONTROLS_ICON_CLICK_LISTENER, INCOGNITO_COOKIE_CONTROLS_TOGGLE_CHECKED,
INCOGNITO_COOKIE_CONTROLS_TOGGLE_CHECKED_LISTENER,
......
......@@ -136,6 +136,14 @@ class TasksView extends CoordinatorLayoutForPointer {
return findViewById(R.id.tasks_surface_body);
}
/**
* Set the visibility of the tasks surface body.
* @param isVisible Whether it's visible.
*/
void setSurfaceBodyVisibility(boolean isVisible) {
getBodyViewContainer().setVisibility(isVisible ? View.VISIBLE : View.GONE);
}
/**
* Set the visibility of the tab carousel.
* @param isVisible Whether it's visible.
......@@ -190,8 +198,11 @@ class TasksView extends CoordinatorLayoutForPointer {
*/
void initializeIncognitoDescriptionView() {
assert mIncognitoDescriptionView == null;
ViewStub stub = (ViewStub) findViewById(R.id.incognito_description_layout_stub);
mIncognitoDescriptionView = (IncognitoDescriptionView) stub.inflate();
ViewStub containerStub =
(ViewStub) findViewById(R.id.incognito_description_container_layout_stub);
View containerView = containerStub.inflate();
mIncognitoDescriptionView = (IncognitoDescriptionView) containerView.findViewById(
R.id.new_tab_incognito_container);
if (mIncognitoDescriptionLearnMoreListener != null) {
setIncognitoDescriptionLearnMoreClickListener(mIncognitoDescriptionLearnMoreListener);
}
......
......@@ -17,6 +17,7 @@ import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_FAKE_S
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO_DESCRIPTION_INITIALIZED;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO_DESCRIPTION_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_SURFACE_BODY_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_TAB_CAROUSEL_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_VOICE_RECOGNITION_BUTTON_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.MORE_TABS_CLICK_LISTENER;
......@@ -74,6 +75,8 @@ class TasksViewBinder {
model.get(INCOGNITO_COOKIE_CONTROLS_MANAGER).updateIfNecessary();
}
view.setIncognitoDescriptionVisibility(isVisible);
} else if (propertyKey == IS_SURFACE_BODY_VISIBLE) {
view.setSurfaceBodyVisibility(model.get(IS_SURFACE_BODY_VISIBLE));
} else if (propertyKey == IS_TAB_CAROUSEL_VISIBLE) {
view.setTabCarouselVisibility(model.get(IS_TAB_CAROUSEL_VISIBLE));
} else if (propertyKey == IS_VOICE_RECOGNITION_BUTTON_VISIBLE) {
......
......@@ -222,12 +222,12 @@ public class TasksViewBinderTest extends DummyUiActivityTestCase {
@Test
@SmallTest
public void testSetIncognitoDescriptionVisibilityAndClickListener() {
assertFalse(isViewVisible(R.id.incognito_description_layout_stub));
assertFalse(isViewVisible(R.id.incognito_description_container_layout_stub));
TestThreadUtils.runOnUiThreadBlocking(() -> {
mTasksViewPropertyModel.set(INCOGNITO_LEARN_MORE_CLICK_LISTENER, mViewOnClickListener);
});
assertFalse(isViewVisible(R.id.incognito_description_layout_stub));
assertFalse(isViewVisible(R.id.incognito_description_container_layout_stub));
TestThreadUtils.runOnUiThreadBlocking(() -> {
mTasksViewPropertyModel.set(INCOGNITO_COOKIE_CONTROLS_MANAGER, mCookieControlsManager);
......
......@@ -21,6 +21,7 @@ import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.INCOGNITO
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_FAKE_SEARCH_BOX_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO_DESCRIPTION_INITIALIZED;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_INCOGNITO_DESCRIPTION_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_SURFACE_BODY_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_TAB_CAROUSEL_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.IS_VOICE_RECOGNITION_BUTTON_VISIBLE;
import static org.chromium.chrome.browser.tasks.TasksSurfaceProperties.VOICE_SEARCH_BUTTON_CLICK_LISTENER;
......@@ -99,6 +100,7 @@ public class TasksSurfaceMediatorUnitTest {
mVoiceSearchButtonClickListenerCaptor.capture());
verify(mPropertyModel).set(eq(IS_FAKE_SEARCH_BOX_VISIBLE), eq(true));
verify(mPropertyModel).set(eq(IS_VOICE_RECOGNITION_BUTTON_VISIBLE), eq(false));
verify(mPropertyModel).set(eq(IS_SURFACE_BODY_VISIBLE), eq(true));
verify(mPropertyModel)
.set(eq(INCOGNITO_COOKIE_CONTROLS_MANAGER), mCookieControlsManagerCaptor.capture());
verify(mPropertyModel)
......
......@@ -906,6 +906,12 @@ public class ChromeTabbedActivity extends ChromeActivity<ChromeActivityComponent
public void onResumeWithNative() {
super.onResumeWithNative();
// Switch to non incognito tab model to show the non incognito start surface if needed.
if (StartSurfaceConfiguration.isStartSurfaceStackTabSwitcherEnabled() && isWarmOnResume()
&& shouldShowTabSwitcherOnStart() && mTabModelSelectorImpl.isIncognitoSelected()) {
mTabModelSelectorImpl.selectModel(false);
}
if (IncognitoUtils.shouldDestroyIncognitoProfileOnStartup(
getTabModelSelector().getCurrentModel().isIncognito())) {
Profile.getLastUsedRegularProfile().getOffTheRecordProfile().destroyWhenAppropriate();
......
......@@ -7,6 +7,7 @@ package org.chromium.chrome.browser.tasks;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static org.chromium.chrome.browser.tasks.ReturnToChromeExperimentsUtil.TAB_SWITCHER_ON_RETURN_MS_PARAM;
......@@ -54,7 +55,9 @@ import org.chromium.chrome.features.start_surface.InstantStartTest;
import org.chromium.chrome.features.start_surface.StartSurfaceConfiguration;
import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ApplicationTestUtils;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
import org.chromium.components.embedder_support.util.UrlConstants;
......@@ -266,6 +269,79 @@ public class ReturnToChromeTest {
}
}
/**
* Test that overview mode is triggered in Single-pane stack tab switcher variation in non
* incognito mode when resuming from incognito mode.
*/
@Test
@SmallTest
@Feature({"ReturnToChrome"})
// clang-format off
@CommandLineFlags.Add({BASE_PARAMS + "/" + TAB_SWITCHER_ON_RETURN_MS_PARAM + "/0"
+ "/start_surface_variation/single/show_last_active_tab_only/true"
+ "/show_stack_tab_switcher/true/open_ntp_instead_of_start/true"})
@DisableIf.Device(type = {UiDisableIf.TABLET}) // See https://crbug.com/1081754.
public void testTabSwitcherModeTriggeredWithinThreshold_WarmStart_FromIncognito_V2() throws Exception {
// clang-format on
// TODO(crbug.com/1093506): Remove it when instant start supports 'show_stack_tab_switcher =
// true'.
assumeFalse(CachedFeatureFlags.isEnabled(ChromeFeatureList.INSTANT_START));
testTabSwitcherModeTriggeredBeyondThreshold();
ChromeTabUtils.newTabFromMenu(InstrumentationRegistry.getInstrumentation(),
mActivityTestRule.getActivity(), true, true);
Assert.assertTrue(
mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
assertEquals(3, mActivityTestRule.getActivity().getTabModelSelector().getTotalTabCount());
// Trigger hide and resume.
ApplicationTestUtils.fireHomeScreenIntent(InstrumentationRegistry.getTargetContext());
mActivityTestRule.startMainActivityFromLauncher();
Assert.assertFalse(
mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
assertEquals(3, mActivityTestRule.getActivity().getTabModelSelector().getTotalTabCount());
Assert.assertTrue(mActivityTestRule.getActivity().getLayoutManager().overviewVisible());
}
/**
* Test that overview mode is triggered in Single-pane non stack tab switcher variation in
* incognito mode when resuming from incognito mode.
*/
@Test
@SmallTest
@Feature({"ReturnToChrome"})
// clang-format off
@CommandLineFlags.Add({BASE_PARAMS + "/" + TAB_SWITCHER_ON_RETURN_MS_PARAM + "/0"
+ "/start_surface_variation/single/"
+ "show_last_active_tab_only/true/open_ntp_instead_of_start/true"})
@DisableIf.Device(type = {UiDisableIf.TABLET}) // See https://crbug.com/1081754.
public void testTabSwitcherModeTriggeredWithinThreshold_WarmStart_FromIncognito_NON_V2() throws Exception {
// clang-format on
// TODO(crbug.com/1095637): Make it work for instant start.
assumeFalse(CachedFeatureFlags.isEnabled(ChromeFeatureList.INSTANT_START));
testTabSwitcherModeTriggeredBeyondThreshold();
ChromeTabUtils.newTabFromMenu(InstrumentationRegistry.getInstrumentation(),
mActivityTestRule.getActivity(), true, true);
Assert.assertTrue(
mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
assertEquals(3, mActivityTestRule.getActivity().getTabModelSelector().getTotalTabCount());
// Trigger hide and resume.
ApplicationTestUtils.fireHomeScreenIntent(InstrumentationRegistry.getTargetContext());
mActivityTestRule.startMainActivityFromLauncher();
Assert.assertTrue(
mActivityTestRule.getActivity().getTabModelSelector().isIncognitoSelected());
assertEquals(3, mActivityTestRule.getActivity().getTabModelSelector().getTotalTabCount());
Assert.assertTrue(mActivityTestRule.getActivity().getLayoutManager().overviewVisible());
}
/**
* Test that overview mode is triggered if the delay is shorter than the interval between
* stop and start.
......
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