Commit 1bf83fe2 authored by Jan Wilken Dörrie's avatar Jan Wilken Dörrie Committed by Commit Bot

[Touch To Fill] Add Branding Strings

This change adds branding strings for Touch To Fill and includes logic
that selects the right string via a Finch parameter. It also modifies
the padding computations to take the new string into account.

Bug: 1034499
Change-Id: Ib262c12dd1fa4db6e5f0788feb46526ecd99a15c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1971835
Commit-Queue: Jan Wilken Dörrie <jdoerrie@chromium.org>
Reviewed-by: default avatarBoris Sazonov <bsazonov@chromium.org>
Cr-Commit-Position: refs/heads/master@{#726442}
parent d1b927f0
......@@ -3,6 +3,9 @@
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!-- Please update R.dimens.touch_to_fill_sheet_height_single_credential and
R.dimens.touch_to_fill_sheet_height_second_credential when modifying
the height of the credential. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:descendantFocusability="blocksDescendants"
......
......@@ -3,13 +3,15 @@
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!-- Please update R.dimens.touch_to_fill_sheet_height_button when modifying
the margins. -->
<org.chromium.ui.widget.ButtonCompat
xmlns:android="http://schemas.android.com/apk/res/android"
android:descendantFocusability="blocksDescendants"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:minHeight="48dp"
android:gravity="center"
android:text="@string/touch_to_fill_continue"
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 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. -->
<!-- Please update R.dimens.touch_to_fill_sheet_height_branding when modifying
the margins. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/touch_to_fill_branding_message"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_width="match_parent"
android:gravity="center"
android:minHeight="20dp"
android:textAppearance="@style/TextAppearance.BlackBody" />
......@@ -3,6 +3,8 @@
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!-- Please update R.dimens.touch_to_fill_sheet_height_single_credential
when modifying the margins image or text sizes. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
......
......@@ -11,6 +11,8 @@
android:layout_width="match_parent"
android:orientation="vertical">
<!-- Please update R.dimens.touch_to_fill_sheet_height_single_credential
when modifying the margins. -->
<ImageView
android:id="@+id/drag_handlebar"
android:layout_width="wrap_content"
......@@ -23,6 +25,8 @@
android:importantForAccessibility="no"
app:srcCompat="@drawable/drag_handlebar" />
<!-- Please update R.dimens.touch_to_fill_sheet_height_second_credential
when modifying paddingBottom. -->
<android.support.v7.widget.RecyclerView
android:id="@+id/sheet_item_list"
android:layout_width="match_parent"
......
......@@ -14,13 +14,29 @@
+ Handlebar (16dp)
+ Header size (112 dp)
+ Title size and margin (48+16dp)
+ First suggestion (72dp)
+ Bottom padding (16dp) -->
<dimen name="touch_to_fill_sheet_height_single_credential">298dp</dimen>
<!-- Base height (298dp) plus padding (8dp) plus 2nd suggestion (72dp) -->
<dimen name="touch_to_fill_sheet_height_multiple_credentials">378dp</dimen>
<!-- Base height (298dp) plus padding (16dp) plus confirm button (48dp) -->
<dimen name="touch_to_fill_sheet_height_single_credential_with_button">
362dp
</dimen>
+ First suggestion (72dp) -->
<dimen name="touch_to_fill_sheet_height_single_credential">282dp</dimen>
<!-- Top padding between RecyclerView elements (8 dp)
+ Extra suggestion height (72 dp). -->
<dimen name="touch_to_fill_sheet_height_second_credential">80dp</dimen>
<!-- Top padding between RecyclerView elements (8 dp)
+ Top margin (2 dp)
+ button height (48 dp)
+ Bottom margin (2 dp) -->
<dimen name="touch_to_fill_sheet_height_button">60dp</dimen>
<!-- Top padding between RecyclerView elements (8 dp)
+ Top margin (16 dp)
+ Text height (20 dp) -->
<dimen name="touch_to_fill_sheet_height_branding">44dp</dimen>
<!-- Depending on the experiments that are active we might show a call to
action button or branding message to the users, at which point we need
different bottom paddings. They are exposed here, so that they can be
added dynamically, depending on the state of the experiments. -->
<dimen name="touch_to_fill_sheet_bottom_padding_credentials">16dp</dimen>
<dimen name="touch_to_fill_sheet_bottom_padding_button">8dp</dimen>
<dimen name="touch_to_fill_sheet_bottom_padding_branding">16dp</dimen>
</resources>
......@@ -8,7 +8,9 @@ import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.Cr
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FAVICON_OR_FALLBACK;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FORMATTED_ORIGIN;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FIELD_TRIAL_PARAM_BRANDING_MESSAGE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FIELD_TRIAL_PARAM_SHOW_CONFIRMATION_BUTTON;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FooterProperties.BRANDING_MESSAGE_ID;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.FORMATTED_URL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.ORIGIN_SECURE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.SINGLE_CREDENTIAL;
......@@ -86,6 +88,12 @@ class TouchToFillMediator {
sheetItems.add(new ListItem(TouchToFillProperties.ItemType.FILL_BUTTON, model));
}
}
sheetItems.add(new ListItem(TouchToFillProperties.ItemType.FOOTER,
new PropertyModel.Builder(TouchToFillProperties.FooterProperties.ALL_KEYS)
.with(BRANDING_MESSAGE_ID, getBrandingMessageId())
.build()));
mModel.set(VISIBLE, true);
}
......@@ -157,6 +165,25 @@ class TouchToFillMediator {
FIELD_TRIAL_PARAM_SHOW_CONFIRMATION_BUTTON, false);
}
/**
* Returns the id of the branding message to be shown. Returns 0 in case no message should be
* displayed.
*/
private int getBrandingMessageId() {
int id = ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
ChromeFeatureList.TOUCH_TO_FILL_ANDROID, FIELD_TRIAL_PARAM_BRANDING_MESSAGE, 0);
switch (id) {
case 1:
return R.string.touch_to_fill_branding_variation_1;
case 2:
return R.string.touch_to_fill_branding_variation_2;
case 3:
return R.string.touch_to_fill_branding_variation_3;
default:
return 0;
}
}
private PropertyModel createModel(Credential credential) {
return new PropertyModel.Builder(CredentialProperties.ALL_KEYS)
.with(CREDENTIAL, credential)
......
......@@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy;
*/
class TouchToFillProperties {
static final String FIELD_TRIAL_PARAM_SHOW_CONFIRMATION_BUTTON = "show_confirmation_button";
static final String FIELD_TRIAL_PARAM_BRANDING_MESSAGE = "branding_message";
static final PropertyModel.WritableBooleanPropertyKey VISIBLE =
new PropertyModel.WritableBooleanPropertyKey("visible");
......@@ -97,7 +98,14 @@ class TouchToFillProperties {
private HeaderProperties() {}
}
@IntDef({ItemType.HEADER, ItemType.CREDENTIAL, ItemType.FILL_BUTTON})
static class FooterProperties {
static final PropertyModel.ReadableIntPropertyKey BRANDING_MESSAGE_ID =
new PropertyModel.ReadableIntPropertyKey("branding_message_id");
static final PropertyKey[] ALL_KEYS = {BRANDING_MESSAGE_ID};
private FooterProperties() {}
}
@IntDef({ItemType.HEADER, ItemType.CREDENTIAL, ItemType.FILL_BUTTON, ItemType.FOOTER})
@Retention(RetentionPolicy.SOURCE)
@interface ItemType {
/**
......@@ -114,6 +122,11 @@ class TouchToFillProperties {
* The fill button at the end of the sheet that filling more obvious for one suggestion.
*/
int FILL_BUTTON = 3;
/**
* The branding message at the bottom of the sheet to draw attention to the feature.
*/
int FOOTER = 4;
}
/**
......
......@@ -4,10 +4,11 @@
package org.chromium.chrome.browser.touch_to_fill;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FIELD_TRIAL_PARAM_BRANDING_MESSAGE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FIELD_TRIAL_PARAM_SHOW_CONFIRMATION_BUTTON;
import android.content.Context;
import android.support.annotation.DimenRes;
import android.content.res.Resources;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
......@@ -15,6 +16,9 @@ import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.DimenRes;
import androidx.annotation.Px;
import org.chromium.base.Callback;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetContent;
......@@ -156,8 +160,7 @@ class TouchToFillView implements BottomSheetContent {
@Override
public float getHalfHeightRatio() {
return Math.min(mContext.getResources().getDimensionPixelSize(getDesiredSheetHeight()),
mBottomSheetController.getContainerHeight())
return Math.min(getDesiredSheetHeight(), mBottomSheetController.getContainerHeight())
/ (float) mBottomSheetController.getContainerHeight();
}
......@@ -187,18 +190,48 @@ class TouchToFillView implements BottomSheetContent {
}
// TODO(crbug.com/1009331): This should add up the height of all items up to the 2nd credential.
private @DimenRes int getDesiredSheetHeight() {
if (mSheetItemListView.getAdapter() != null
private @Px int getDesiredSheetHeight() {
Resources resources = mContext.getResources();
@Px
int totalHeight = resources.getDimensionPixelSize(
R.dimen.touch_to_fill_sheet_height_single_credential);
final boolean hasMultipleCredentials = mSheetItemListView.getAdapter() != null
&& mSheetItemListView.getAdapter().getItemCount() > 2
&& mSheetItemListView.getAdapter().getItemViewType(2)
== TouchToFillProperties.ItemType.CREDENTIAL) {
return R.dimen.touch_to_fill_sheet_height_multiple_credentials;
== TouchToFillProperties.ItemType.CREDENTIAL;
if (hasMultipleCredentials) {
totalHeight += resources.getDimensionPixelSize(
R.dimen.touch_to_fill_sheet_height_second_credential);
}
if (ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
ChromeFeatureList.TOUCH_TO_FILL_ANDROID,
FIELD_TRIAL_PARAM_SHOW_CONFIRMATION_BUTTON, false)) {
return R.dimen.touch_to_fill_sheet_height_single_credential_with_button;
final boolean hasButton = !hasMultipleCredentials
&& ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
ChromeFeatureList.TOUCH_TO_FILL_ANDROID,
FIELD_TRIAL_PARAM_SHOW_CONFIRMATION_BUTTON, false);
if (hasButton) {
totalHeight +=
resources.getDimensionPixelSize(R.dimen.touch_to_fill_sheet_height_button);
}
final boolean hasBranding = ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
ChromeFeatureList.TOUCH_TO_FILL_ANDROID,
FIELD_TRIAL_PARAM_BRANDING_MESSAGE, 0)
!= 0;
if (hasBranding) {
totalHeight +=
resources.getDimensionPixelSize(R.dimen.touch_to_fill_sheet_height_branding);
}
return R.dimen.touch_to_fill_sheet_height_single_credential;
return totalHeight + getDesiredBottomPadding(hasButton, hasBranding);
}
private @Px int getDesiredBottomPadding(boolean hasButton, boolean hasBranding) {
@DimenRes
int bottomPaddingId = R.dimen.touch_to_fill_sheet_bottom_padding_credentials;
if (hasButton) bottomPaddingId = R.dimen.touch_to_fill_sheet_bottom_padding_button;
if (hasBranding) bottomPaddingId = R.dimen.touch_to_fill_sheet_bottom_padding_branding;
return mContext.getResources().getDimensionPixelSize(bottomPaddingId);
}
}
......@@ -9,6 +9,7 @@ import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.Cr
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FORMATTED_ORIGIN;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.DISMISS_HANDLER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FooterProperties.BRANDING_MESSAGE_ID;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.FORMATTED_URL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.ORIGIN_SECURE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.SINGLE_CREDENTIAL;
......@@ -17,6 +18,7 @@ import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.SH
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VISIBLE;
import static org.chromium.chrome.browser.util.UrlUtilities.stripScheme;
import android.content.Context;
import android.text.method.PasswordTransformationMethod;
import android.view.View;
import android.view.ViewGroup;
......@@ -87,6 +89,9 @@ class TouchToFillViewBinder {
case ItemType.FILL_BUTTON:
return new TouchToFillViewHolder(parent, R.layout.touch_to_fill_fill_button,
TouchToFillViewBinder::bindFillButtonView);
case ItemType.FOOTER:
return new TouchToFillViewHolder(parent, R.layout.touch_to_fill_footer,
TouchToFillViewBinder::bindFooterView);
}
assert false : "Cannot create view for ItemType: " + itemType;
return null;
......@@ -199,5 +204,22 @@ class TouchToFillViewBinder {
}
}
private static void bindFooterView(PropertyModel model, View view, PropertyKey key) {
if (key == BRANDING_MESSAGE_ID) {
TextView brandingMessage = view.findViewById(R.id.touch_to_fill_branding_message);
@StringRes
int messageId = model.get(BRANDING_MESSAGE_ID);
if (messageId == 0) {
brandingMessage.setVisibility(View.GONE);
} else {
Context context = view.getContext();
brandingMessage.setText(String.format(context.getString(messageId),
context.getString(org.chromium.chrome.R.string.app_name)));
}
} else {
assert false : "Unhandled update to property:" + key;
}
}
private TouchToFillViewBinder() {}
}
......@@ -171,6 +171,15 @@
<message name="IDS_TOUCH_TO_FILL_CONTINUE" desc="Title of the button that continues filling with the only available set of credentials.">
Continue
</message>
<message name="IDS_TOUCH_TO_FILL_BRANDING_VARIATION_1" desc="Message shown at the bottom of the Touch To Fill sheet to draw attention to the feature.">
New in <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph>! Tap to sign in
</message>
<message name="IDS_TOUCH_TO_FILL_BRANDING_VARIATION_2" desc="Message shown at the bottom of the Touch To Fill sheet to draw attention to the feature.">
New in <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph>! Control how you sign in with one tap
</message>
<message name="IDS_TOUCH_TO_FILL_BRANDING_VARIATION_3" desc="Message shown at the bottom of the Touch To Fill sheet to draw attention to the feature.">
Powered by <ph name="APP_NAME">%1$s<ex>Chrome</ex></ph>
</message>
</messages>
</release>
</grit>
......@@ -15,6 +15,7 @@ import static org.mockito.Mockito.verify;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.CREDENTIAL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FORMATTED_ORIGIN;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FooterProperties.BRANDING_MESSAGE_ID;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.FORMATTED_URL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.ORIGIN_SECURE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties.SINGLE_CREDENTIAL;
......@@ -43,6 +44,7 @@ import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.ScalableTimeout;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FooterProperties;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.HeaderProperties;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
......@@ -187,6 +189,46 @@ public class TouchToFillViewTest {
assertThat(subtitle.getText(), is(getFormattedNotSecureSubtitle("m.example.org")));
}
@Test
@MediumTest
public void testBrandingVariationZeroHides() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mModel.get(SHEET_ITEMS)
.add(new MVCListAdapter.ListItem(TouchToFillProperties.ItemType.FOOTER,
new PropertyModel.Builder(FooterProperties.ALL_KEYS)
.with(BRANDING_MESSAGE_ID, 0)
.build()));
mModel.set(VISIBLE, true);
});
pollUiThread(() -> getBottomSheetState() == BottomSheetController.SheetState.HALF);
TextView brandingMessage =
mTouchToFillView.getContentView().findViewById(R.id.touch_to_fill_branding_message);
assertThat(brandingMessage.getVisibility(), is(View.GONE));
}
@Test
@MediumTest
public void testBrandingVariationOneDisplayed() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mModel.get(SHEET_ITEMS)
.add(new MVCListAdapter.ListItem(TouchToFillProperties.ItemType.FOOTER,
new PropertyModel.Builder(FooterProperties.ALL_KEYS)
.with(BRANDING_MESSAGE_ID,
R.string.touch_to_fill_branding_variation_1)
.build()));
mModel.set(VISIBLE, true);
});
pollUiThread(() -> getBottomSheetState() == BottomSheetController.SheetState.HALF);
TextView brandingMessage =
mTouchToFillView.getContentView().findViewById(R.id.touch_to_fill_branding_message);
String expectedBrandingMessage =
String.format(getActivity().getString(R.string.touch_to_fill_branding_variation_1),
getActivity().getString(org.chromium.chrome.R.string.app_name));
assertThat(brandingMessage.getText(), is(expectedBrandingMessage));
}
@Test
@MediumTest
public void testCredentialsChangedByModel() {
......
......@@ -149,7 +149,7 @@ public class TouchToFillControllerTest {
public void testShowCredentialsSetsCredentialListAndRequestsFavicons() {
mMediator.showCredentials(TEST_URL, true, Arrays.asList(ANA, CARL, BOB));
ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
assertThat(itemList.size(), is(4));
assertThat(itemList.size(), is(5)); // Header + Credentials + Footer
assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
assertThat(itemList.get(1).model.get(CREDENTIAL), is(ANA));
assertThat(itemList.get(1).model.get(FAVICON_OR_FALLBACK), is(nullValue()));
......@@ -172,7 +172,7 @@ public class TouchToFillControllerTest {
public void testFetchFaviconUpdatesModel() {
mMediator.showCredentials(TEST_URL, true, Collections.singletonList(CARL));
ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
assertThat(itemList.size(), is(2));
assertThat(itemList.size(), is(3)); // Header + Credentials + Footer
assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
assertThat(itemList.get(1).model.get(CREDENTIAL), is(CARL));
assertThat(itemList.get(1).model.get(FAVICON_OR_FALLBACK), is(nullValue()));
......@@ -197,7 +197,7 @@ public class TouchToFillControllerTest {
@Test
public void testShowCredentialsFormatPslOrigins() {
mMediator.showCredentials(TEST_URL, true, Arrays.asList(ANA, BOB));
assertThat(mModel.get(SHEET_ITEMS).size(), is(3));
assertThat(mModel.get(SHEET_ITEMS).size(), is(4)); // Header + Credentials + Footer
assertThat(mModel.get(SHEET_ITEMS).get(1).type, is(ItemType.CREDENTIAL));
assertThat(mModel.get(SHEET_ITEMS).get(1).model.get(FORMATTED_ORIGIN),
is(format(ANA.getOriginUrl())));
......@@ -210,7 +210,7 @@ public class TouchToFillControllerTest {
public void testClearsCredentialListWhenShowingAgain() {
mMediator.showCredentials(TEST_URL, true, Collections.singletonList(ANA));
ListModel<MVCListAdapter.ListItem> itemList = mModel.get(SHEET_ITEMS);
assertThat(itemList.size(), is(2));
assertThat(itemList.size(), is(3)); // Header + Credentials + Footer
assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
assertThat(itemList.get(1).model.get(CREDENTIAL), is(ANA));
assertThat(itemList.get(1).model.get(FAVICON_OR_FALLBACK), is(nullValue()));
......@@ -218,7 +218,7 @@ public class TouchToFillControllerTest {
// Showing the sheet a second time should replace all changed credentials.
mMediator.showCredentials(TEST_URL, true, Collections.singletonList(BOB));
itemList = mModel.get(SHEET_ITEMS);
assertThat(itemList.size(), is(2));
assertThat(itemList.size(), is(3)); // Header + Credentials + Footer
assertThat(itemList.get(1).type, is(ItemType.CREDENTIAL));
assertThat(itemList.get(1).model.get(CREDENTIAL), is(BOB));
assertThat(itemList.get(1).model.get(FAVICON_OR_FALLBACK), is(nullValue()));
......
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