Commit 428a35af authored by Wenyu Fu's avatar Wenyu Fu Committed by Commit Bot

Add accessibility support for History page

When the accessibility mode is on, or a hardware keyboard is attached to
the device, it will disable the scroll to load functionality on History
activity, and a footer contains MoreProgressButton
(browser/ui/widget/MoreProgressButton) will be added and bounded with
HistoryAdapter#loadMoreItems.

Unit test for HistoryAdapter and HistoryActivity is also included.

Bug: 1001286
Change-Id: I15d68624228215898db73bbd0898367b5900f555
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1891570
Commit-Queue: Wenyu Fu <wenyufu@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#713087}
parent 301854c9
......@@ -194,7 +194,10 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/hardware_acceleration/Utils.java",
"javatests/src/org/chromium/chrome/browser/hardware_acceleration/WebappActivityHWATest.java",
"javatests/src/org/chromium/chrome/browser/history/HistoryActivityTest.java",
"javatests/src/org/chromium/chrome/browser/history/HistoryActivityScrollingTest.java",
"javatests/src/org/chromium/chrome/browser/history/HistoryAdapterTest.java",
"javatests/src/org/chromium/chrome/browser/history/HistoryAdapterAccessibilityTest.java",
"javatests/src/org/chromium/chrome/browser/history/HistoryTestUtils.java",
"javatests/src/org/chromium/chrome/browser/history/StubbedHistoryProvider.java",
"javatests/src/org/chromium/chrome/browser/history/TestBrowsingHistoryObserver.java",
"javatests/src/org/chromium/chrome/browser/identity/SettingsSecureBasedIdentificationGeneratorTest.java",
......
......@@ -24,6 +24,8 @@ import org.chromium.chrome.browser.favicon.FaviconHelper.DefaultFaviconHelper;
import org.chromium.chrome.browser.history.HistoryProvider.BrowsingHistoryObserver;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.chrome.browser.ui.widget.MoreProgressButton;
import org.chromium.chrome.browser.ui.widget.MoreProgressButton.State;
import org.chromium.chrome.browser.util.UrlConstants;
import org.chromium.chrome.browser.widget.DateDividedAdapter;
import org.chromium.chrome.browser.widget.selection.SelectableItemViewHolder;
......@@ -48,11 +50,16 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
private RecyclerView mRecyclerView;
private @Nullable HistoryProvider mHistoryProvider;
// Headers
private View mPrivacyDisclaimerBottomSpace;
private Button mClearBrowsingDataButton;
private HeaderItem mPrivacyDisclaimerHeaderItem;
private HeaderItem mClearBrowsingDataButtonHeaderItem;
// Footers
private MoreProgressButton mMoreProgressButton;
private FooterItem mMoreProgressButtonFooterItem;
private boolean mHasOtherFormsOfBrowsingData;
private boolean mIsDestroyed;
private boolean mAreHeadersInitialized;
......@@ -64,6 +71,8 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
private boolean mClearBrowsingDataButtonVisible;
private String mQueryText = EMPTY_QUERY;
private boolean mDisableScrollToLoadForTest;
public HistoryAdapter(SelectionDelegate<HistoryItem> delegate, HistoryManager manager,
HistoryProvider provider) {
setHasStableIds(true);
......@@ -116,10 +125,11 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
* there are no more items to load.
*/
public void loadMoreItems() {
if (!canLoadMoreItems()) return;
if (!canLoadMoreItems()) {
return;
}
mIsLoadingItems = true;
addFooter();
updateFooter();
notifyDataSetChanged();
mHistoryProvider.queryHistoryContinuation();
}
......@@ -244,6 +254,8 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
mIsLoadingItems = false;
mHasMorePotentialItems = hasMorePotentialMatches;
if (mHasMorePotentialItems) updateFooter();
}
@Override
......@@ -265,9 +277,45 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
@Override
protected BasicViewHolder createFooter(ViewGroup parent) {
return new BasicViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.indeterminate_progress_view, parent, false));
// Create the same frame layout as place holder for more footer items.
return createHeader(parent);
}
/**
* Initialize a more progress button as footer items that will be re-used
* during page loading.
*/
void generateFooterItems() {
mMoreProgressButton = (MoreProgressButton) View.inflate(
mHistoryManager.getSelectableListLayout().getContext(),
R.layout.more_progress_button, null);
mMoreProgressButton.setOnClickRunnable(this::loadMoreItems);
mMoreProgressButtonFooterItem = new FooterItem(-1, mMoreProgressButton);
}
@Override
public void addFooter() {
if (hasListFooter()) return;
ItemGroup footer = new FooterItemGroup();
footer.addItem(mMoreProgressButtonFooterItem);
// When scroll to load is enabled, the footer just added should be set to spinner.
// When scroll to load is disabled, the footer just added should first display the button.
if (isScrollToLoadDisabled()) {
mMoreProgressButton.setState(State.BUTTON);
} else {
mMoreProgressButton.setState(State.LOADING);
}
addGroup(footer);
}
/**
* Update footer when the content change.
*/
private void updateFooter() {
if (isScrollToLoadDisabled() || mIsLoadingItems) addFooter();
}
/**
......@@ -336,6 +384,14 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
return !mHistoryManager.isIncognito() && mHasOtherFormsOfBrowsingData;
}
/**
* @return True if HistoryManager is not null, and scroll to load is disabled in HistoryManager
*/
boolean isScrollToLoadDisabled() {
return mDisableScrollToLoadForTest
|| (mHistoryManager != null && mHistoryManager.isScrollToLoadDisabled());
}
/**
* Set text of privacy disclaimer and visibility of its container.
*/
......@@ -366,6 +422,12 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
return getGroupAt(0).first;
}
@VisibleForTesting
ItemGroup getLastGroupForTests() {
final int itemCount = getItemCount();
return itemCount > 0 ? getGroupAt(itemCount - 1).first : null;
}
@VisibleForTesting
void setClearBrowsingDataButtonVisibilityForTest(boolean isVisible) {
if (mClearBrowsingDataButtonVisible == isVisible) return;
......@@ -386,6 +448,12 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
mClearBrowsingDataButtonVisible = true;
}
@VisibleForTesting
void generateFooterItemsForTest(MoreProgressButton mockButton) {
mMoreProgressButton = mockButton;
mMoreProgressButtonFooterItem = new FooterItem(-1, null);
}
@VisibleForTesting
boolean arePrivacyDisclaimersVisible() {
return mPrivacyDisclaimersVisible;
......@@ -395,4 +463,14 @@ public class HistoryAdapter extends DateDividedAdapter implements BrowsingHistor
boolean isClearBrowsingDataButtonVisible() {
return mClearBrowsingDataButtonVisible;
}
@VisibleForTesting
void setScrollToLoadDisabledForTest(boolean isDisabled) {
mDisableScrollToLoadForTest = isDisabled;
}
@VisibleForTesting
MoreProgressButton getMoreProgressButtonForTest() {
return mMoreProgressButton;
}
}
......@@ -45,6 +45,7 @@ import org.chromium.chrome.browser.snackbar.SnackbarManager;
import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabLaunchType;
import org.chromium.chrome.browser.util.AccessibilityUtil;
import org.chromium.chrome.browser.util.ConversionUtils;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.chrome.browser.widget.selection.SelectableListLayout;
......@@ -83,10 +84,12 @@ public class HistoryManager implements OnMenuItemClickListener, SignInStateObser
private static final int PAGE_TRANSITION_TYPE = PageTransition.AUTO_BOOKMARK;
private static HistoryProvider sProviderForTests;
private static Boolean sIsScrollToLoadDisabledForTests;
private final Activity mActivity;
private final boolean mIsIncognito;
private final boolean mIsSeparateActivity;
private final boolean mIsScrollToLoadDisabled;
private final SelectableListLayout<HistoryItem> mSelectableListLayout;
private final HistoryAdapter mHistoryAdapter;
private final SelectionDelegate<HistoryItem> mSelectionDelegate;
......@@ -117,6 +120,9 @@ public class HistoryManager implements OnMenuItemClickListener, SignInStateObser
mIsSeparateActivity = isSeparateActivity;
mSnackbarManager = snackbarManager;
mIsIncognito = isIncognito;
mIsScrollToLoadDisabled = AccessibilityUtil.isAccessibilityEnabled()
|| AccessibilityUtil.isHardwareKeyboardAttached(
mActivity.getResources().getConfiguration());
mSelectionDelegate = new SelectionDelegate<>();
mSelectionDelegate.addObserver(this);
......@@ -160,6 +166,7 @@ public class HistoryManager implements OnMenuItemClickListener, SignInStateObser
// 7. Initialize the adapter to load items.
mHistoryAdapter.generateHeaderItems();
mHistoryAdapter.generateFooterItems();
mHistoryAdapter.initialize();
// 8. Add scroll listener to show/hide info button on scroll and page in more items
......@@ -174,7 +181,10 @@ public class HistoryManager implements OnMenuItemClickListener, SignInStateObser
mToolbar.updateInfoMenuItem(
shouldShowInfoButton(), shouldShowInfoHeaderIfAvailable());
if (!mHistoryAdapter.canLoadMoreItems()) return;
if (!mHistoryAdapter.canLoadMoreItems() || isScrollToLoadDisabled()) {
return;
}
// Load more items if the scroll position is close to the bottom of the list.
if (layoutManager.findLastVisibleItemPosition()
> (mHistoryAdapter.getItemCount() - 25)) {
......@@ -511,6 +521,19 @@ public class HistoryManager implements OnMenuItemClickListener, SignInStateObser
return mShouldShowInfoHeader;
}
/**
* Check if we want to enable the scrolling to load for recycled view. Noting this function
* will be called during testing with RecycledView == null. Will return False in such case.
* @return True if accessibility is enabled or a hardware keyboard is attached.
*/
boolean isScrollToLoadDisabled() {
if (sIsScrollToLoadDisabledForTests != null) {
return sIsScrollToLoadDisabledForTests.booleanValue();
}
return mIsScrollToLoadDisabled;
}
@Override
public void onSignedIn() {
mToolbar.onSignInStateChange();
......@@ -553,4 +576,9 @@ public class HistoryManager implements OnMenuItemClickListener, SignInStateObser
public RecyclerView getRecyclerViewForTests() {
return mRecyclerView;
}
@VisibleForTesting
public static void setScrollToLoadDisabledForTesting(boolean isScrollToLoadDisabled) {
sIsScrollToLoadDisabledForTests = isScrollToLoadDisabled;
}
}
......@@ -144,6 +144,21 @@ public abstract class DateDividedAdapter extends Adapter<RecyclerView.ViewHolder
}
}
/**
* Contains information of a single footer that this adapter uses to manage footers.
* Share most of the same funcionality as a Header class.
*/
public static class FooterItem extends HeaderItem {
public FooterItem(int position, View view) {
super(position, view);
}
@Override
public long getTimestamp() {
return Long.MIN_VALUE;
}
}
/** An item representing a date header. */
class DateHeaderTimedItem extends TimedItem {
private long mTimestamp;
......@@ -350,21 +365,6 @@ public abstract class DateDividedAdapter extends Adapter<RecyclerView.ViewHolder
/** An item group representing the list footer(s). */
public static class FooterItemGroup extends ItemGroup {
public FooterItemGroup() {
super();
addItem(new TimedItem() {
@Override
public long getTimestamp() {
return Long.MIN_VALUE;
}
@Override
public long getStableId() {
return getTimestamp();
}
});
}
@Override
public @GroupPriority int priority() {
return GroupPriority.FOOTER;
......@@ -504,6 +504,18 @@ public abstract class DateDividedAdapter extends Adapter<RecyclerView.ViewHolder
((ViewGroup) basicViewHolder.itemView).addView(v);
}
/**
* Binds the {@link BasicViewHolder} with the given {@link FooterItem}.
* @see #onBindViewHolder(ViewHolder, int)
*/
protected void bindViewHolderForFooterItem(ViewHolder viewHolder, FooterItem footerItem) {
BasicViewHolder basicViewHolder = (BasicViewHolder) viewHolder;
View v = footerItem.getView();
((ViewGroup) basicViewHolder.itemView).removeAllViews();
if (v.getParent() != null) ((ViewGroup) v.getParent()).removeView(v);
((ViewGroup) basicViewHolder.itemView).addView(v);
}
/**
* Gets the resource id of the view showing the date header.
* Contract for subclasses: this view should be a {@link TextView}.
......@@ -714,7 +726,7 @@ public abstract class DateDividedAdapter extends Adapter<RecyclerView.ViewHolder
bindViewHolderForHeaderItem(holder, (HeaderItem) pair.second);
break;
case ItemViewType.FOOTER:
// Do nothing.
bindViewHolderForFooterItem(holder, (FooterItem) pair.second);
break;
case ItemViewType.SUBSECTION_HEADER:
bindViewHolderForSubsectionHeader((SubsectionHeaderViewHolder) holder, pair.second);
......
// 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.
package org.chromium.chrome.browser.history;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.filters.SmallTest;
import android.support.v7.widget.RecyclerView;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.params.ParameterAnnotations;
import org.chromium.base.test.params.ParameterProvider;
import org.chromium.base.test.params.ParameterSet;
import org.chromium.base.test.params.ParameterizedRunner;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.history.HistoryTestUtils.TestObserver;
import org.chromium.chrome.browser.ui.widget.MoreProgressButton;
import org.chromium.chrome.browser.ui.widget.MoreProgressButton.State;
import org.chromium.chrome.browser.widget.DateDividedAdapter.FooterItem;
import org.chromium.chrome.browser.widget.DateDividedAdapter.TimedItem;
import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
import org.chromium.chrome.test.util.browser.RecyclerViewTestUtils;
import org.chromium.chrome.test.util.browser.signin.SigninTestUtil;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.ui.test.util.UiRestriction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* Tests the scrolling behavior on {@link HistoryActivity}.
* The main difference for this test file with {@link HistoryActivityTest}is to test scrolling
* behavior under different settings.
*/
// clang-format off
@RunWith(ParameterizedRunner.class)
@ParameterAnnotations.UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
public class HistoryActivityScrollingTest {
// clang-format on
@Rule
public IntentsTestRule<HistoryActivity> mActivityTestRule =
new IntentsTestRule<>(HistoryActivity.class, false, false);
@ParameterAnnotations.ClassParameter
private static List<ParameterSet> sClassParams = new TestParamsProvider().getParameters();
private static class TestParams extends ParameterSet {
public final int mPaging;
public final int mTotalItems;
public final boolean mIsScrollToLoadDisabled;
public TestParams(int paging, int totalItems, boolean isScrollToLoadDisabled) {
super();
mPaging = paging;
mTotalItems = totalItems;
mIsScrollToLoadDisabled = isScrollToLoadDisabled;
value(paging, totalItems, isScrollToLoadDisabled);
}
@Override
public String toString() {
return "paging: " + mPaging + ", totalItems: " + mTotalItems
+ ", isScrollToLoadDisabled: " + mIsScrollToLoadDisabled;
}
}
private static class TestParamsProvider implements ParameterProvider {
private static List<ParameterSet> sScrollToLoad =
Arrays.asList(new TestParams(5, 30, false).name("Enabled"),
new TestParams(5, 30, true).name("Disabled"),
new TestParams(5, 12, true).name("Disabled_Less"));
@Override
public List<ParameterSet> getParameters() {
return sScrollToLoad;
}
}
private StubbedHistoryProvider mHistoryProvider;
private HistoryAdapter mAdapter;
private HistoryManager mHistoryManager;
private RecyclerView mRecyclerView;
private TestObserver mTestObserver;
// private PrefChangeRegistrar mPrefChangeRegistrar;
private List<HistoryItem> mItems;
private int mPaging;
private int mTotalItems;
private boolean mIsScrollToLoadDisabled;
private int mOrigItemsCount;
public HistoryActivityScrollingTest(
int paging, int totalItems, boolean isScrollToLoadDisabled) {
mPaging = paging;
mTotalItems = totalItems;
mIsScrollToLoadDisabled = isScrollToLoadDisabled;
mItems = new ArrayList<>(mTotalItems);
}
@Before
public void setUp() throws Exception {
// Account not signed in by default. The clear browsing data header, one date view, and two
// history item views should be shown, but the info header should not. We enforce a defaultx
// state because the number of headers shown depends on the signed-in state.
SigninTestUtil.setUpAuthForTest();
mHistoryProvider = new StubbedHistoryProvider();
mHistoryProvider.setPaging(mPaging);
Date today = new Date();
long timestamp = today.getTime();
for (int i = 0; i < mTotalItems; i++) {
HistoryItem item = StubbedHistoryProvider.createHistoryItem(0, --timestamp);
mItems.add(item);
mHistoryProvider.addItem(item);
}
HistoryManager.setProviderForTests(mHistoryProvider);
HistoryManager.setScrollToLoadDisabledForTesting(mIsScrollToLoadDisabled);
launchHistoryActivity();
HistoryTestUtils.setupHistoryTestHeaders(mAdapter, mTestObserver);
mOrigItemsCount = mAdapter.getItemCount();
Assert.assertTrue("At least one item should be loaded to adapter", mOrigItemsCount > 0);
}
@After
public void tearDown() {
SigninTestUtil.tearDownAuthForTest();
}
private void launchHistoryActivity() {
HistoryActivity activity = mActivityTestRule.launchActivity(null);
mHistoryManager = activity.getHistoryManagerForTests();
mAdapter = mHistoryManager.getAdapterForTests();
mTestObserver = new TestObserver();
mHistoryManager.getSelectionDelegateForTests().addObserver(mTestObserver);
mAdapter.registerAdapterDataObserver(mTestObserver);
mRecyclerView = ((RecyclerView) activity.findViewById(R.id.recycler_view));
}
@Test
@SmallTest
public void testScrollToLoadEnabled() {
assumeFalse(mIsScrollToLoadDisabled);
RecyclerViewTestUtils.scrollToBottom(mRecyclerView);
Assert.assertTrue("Should load more items into view after scroll",
mAdapter.getItemCount() > mOrigItemsCount);
Assert.assertTrue(String.valueOf(mPaging) + " more Items should be loaded",
mAdapter.getItemCount() == mOrigItemsCount + mPaging);
}
@Test
@SmallTest
public void testScrollToLoadDisabled() throws Exception {
assumeTrue(mIsScrollToLoadDisabled);
RecyclerViewTestUtils.scrollToBottom(mRecyclerView);
Assert.assertTrue("Should not load more items into view after scroll",
mAdapter.getItemCount() == mOrigItemsCount);
Assert.assertTrue(
"Footer should be added to the end of the view", mAdapter.hasListFooter());
Assert.assertEquals(
"Footer group should contain one item", 1, mAdapter.getLastGroupForTests().size());
// Verify the button is correctly displayed
TimedItem item = mAdapter.getLastGroupForTests().getItemAt(0);
MoreProgressButton button = (MoreProgressButton) ((FooterItem) item).getView();
Assert.assertSame("FooterItem view should be MoreProgressButton",
mAdapter.getMoreProgressButtonForTest(), button);
Assert.assertEquals(
"State for the MPB should be button", button.getStateForTest(), State.BUTTON);
// Test click, should load more items
int callCount = mTestObserver.onChangedCallback.getCallCount();
TestThreadUtils.runOnUiThreadBlocking(
() -> button.findViewById(R.id.action_button).performClick());
mTestObserver.onChangedCallback.waitForCallback(callCount);
Assert.assertTrue("Should load more items into view after click more button",
mAdapter.getItemCount() > mOrigItemsCount);
}
}
......@@ -17,8 +17,6 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.provider.Browser;
import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.filters.SmallTest;
......@@ -36,24 +34,21 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.task.PostTask;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.RetryOnFailure;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.history.HistoryTestUtils.TestObserver;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.preferences.PrefChangeRegistrar;
import org.chromium.chrome.browser.preferences.PrefChangeRegistrar.PrefObserver;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.IdentityServicesProvider;
import org.chromium.chrome.browser.signin.SigninManager.SignInStateObserver;
import org.chromium.chrome.browser.widget.DateDividedAdapter;
import org.chromium.chrome.browser.widget.selection.SelectableItemView;
import org.chromium.chrome.browser.widget.selection.SelectableItemViewHolder;
import org.chromium.chrome.browser.widget.selection.SelectionDelegate.SelectionObserver;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.signin.SigninTestUtil;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
......@@ -67,7 +62,6 @@ import org.chromium.ui.base.PageTransition;
import org.chromium.ui.test.util.UiRestriction;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
......@@ -81,47 +75,6 @@ public class HistoryActivityTest {
public IntentsTestRule<HistoryActivity> mActivityTestRule =
new IntentsTestRule<>(HistoryActivity.class, false, false);
private static class TestObserver extends RecyclerView.AdapterDataObserver
implements SelectionObserver<HistoryItem>, SignInStateObserver, PrefObserver {
public final CallbackHelper onChangedCallback = new CallbackHelper();
public final CallbackHelper onSelectionCallback = new CallbackHelper();
public final CallbackHelper onSigninStateChangedCallback = new CallbackHelper();
public final CallbackHelper onPreferenceChangeCallback = new CallbackHelper();
private Handler mHandler;
public TestObserver() {
mHandler = new Handler(Looper.getMainLooper());
}
@Override
public void onChanged() {
// To guarantee that all real Observers have had a chance to react to the event, post
// the CallbackHelper.notifyCalled() call.
mHandler.post(() -> onChangedCallback.notifyCalled());
}
@Override
public void onSelectionStateChange(List<HistoryItem> selectedItems) {
mHandler.post(() -> onSelectionCallback.notifyCalled());
}
@Override
public void onSignedIn() {
mHandler.post(() -> onSigninStateChangedCallback.notifyCalled());
}
@Override
public void onSignedOut() {
mHandler.post(() -> onSigninStateChangedCallback.notifyCalled());
}
@Override
public void onPreferenceChange() {
mHandler.post(() -> onPreferenceChangeCallback.notifyCalled());
}
}
private StubbedHistoryProvider mHistoryProvider;
private HistoryAdapter mAdapter;
private HistoryManager mHistoryManager;
......@@ -151,18 +104,7 @@ public class HistoryActivityTest {
HistoryManager.setProviderForTests(mHistoryProvider);
launchHistoryActivity();
if (!mAdapter.isClearBrowsingDataButtonVisible()) {
int changedCallCount = mTestObserver.onChangedCallback.getCallCount();
TestThreadUtils.runOnUiThreadBlocking(
() -> mAdapter.setClearBrowsingDataButtonVisibilityForTest(true));
mTestObserver.onChangedCallback.waitForCallback(changedCallCount);
}
if (mAdapter.arePrivacyDisclaimersVisible()) {
int changedCallCount = mTestObserver.onChangedCallback.getCallCount();
setHasOtherFormsOfBrowsingData(false);
mTestObserver.onChangedCallback.waitForCallback(changedCallCount);
}
HistoryTestUtils.setupHistoryTestHeaders(mAdapter, mTestObserver);
Assert.assertEquals(4, mAdapter.getItemCount());
}
......
......@@ -4,16 +4,20 @@
package org.chromium.chrome.browser.history;
import static org.chromium.chrome.browser.history.HistoryTestUtils.checkAdapterContents;
import android.support.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.DateDividedAdapter.ItemViewType;
import org.chromium.chrome.browser.ui.widget.MoreProgressButton;
import org.chromium.chrome.browser.widget.selection.SelectionDelegate;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
......@@ -29,11 +33,16 @@ public class HistoryAdapterTest {
private StubbedHistoryProvider mHistoryProvider;
private HistoryAdapter mAdapter;
@Mock
private MoreProgressButton mMockButton;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mHistoryProvider = new StubbedHistoryProvider();
mAdapter = new HistoryAdapter(new SelectionDelegate<HistoryItem>(), null, mHistoryProvider);
mAdapter.generateHeaderItemsForTest();
mAdapter.generateFooterItemsForTest(mMockButton);
}
private void initializeAdapter() {
......@@ -44,7 +53,7 @@ public class HistoryAdapterTest {
@SmallTest
public void testInitialize_Empty() {
initializeAdapter();
checkAdapterContents(false);
checkAdapterContents(mAdapter, false, false);
}
@Test
......@@ -58,7 +67,7 @@ public class HistoryAdapterTest {
initializeAdapter();
// There should be three items - the header, a date and the history item.
checkAdapterContents(true, null, null, item1);
checkAdapterContents(mAdapter, true, false, null, null, item1);
}
@Test
......@@ -75,19 +84,19 @@ public class HistoryAdapterTest {
initializeAdapter();
// There should be four items - the list header, a date header and two history items.
checkAdapterContents(true, null, null, item1, item2);
checkAdapterContents(mAdapter, true, false, null, null, item1, item2);
mAdapter.markItemForRemoval(item1);
// Check that one item was removed.
checkAdapterContents(true, null, null, item2);
checkAdapterContents(mAdapter, true, false, null, null, item2);
Assert.assertEquals(1, mHistoryProvider.markItemForRemovalCallback.getCallCount());
Assert.assertEquals(0, mHistoryProvider.removeItemsCallback.getCallCount());
mAdapter.markItemForRemoval(item2);
// There should no longer be any items in the adapter.
checkAdapterContents(false);
checkAdapterContents(mAdapter, false, false);
Assert.assertEquals(2, mHistoryProvider.markItemForRemovalCallback.getCallCount());
Assert.assertEquals(0, mHistoryProvider.removeItemsCallback.getCallCount());
......@@ -111,19 +120,19 @@ public class HistoryAdapterTest {
// There should be five items - the list header, a date header, a history item, another
// date header and another history item.
checkAdapterContents(true, null, null, item1, null, item2);
checkAdapterContents(mAdapter, true, false, null, null, item1, null, item2);
mAdapter.markItemForRemoval(item1);
// Check that the first item and date header were removed.
checkAdapterContents(true, null, null, item2);
checkAdapterContents(mAdapter, true, false, null, null, item2);
Assert.assertEquals(1, mHistoryProvider.markItemForRemovalCallback.getCallCount());
Assert.assertEquals(0, mHistoryProvider.removeItemsCallback.getCallCount());
mAdapter.markItemForRemoval(item2);
// There should no longer be any items in the adapter.
checkAdapterContents(false);
checkAdapterContents(mAdapter, false, false);
Assert.assertEquals(2, mHistoryProvider.markItemForRemovalCallback.getCallCount());
Assert.assertEquals(0, mHistoryProvider.removeItemsCallback.getCallCount());
......@@ -144,17 +153,17 @@ public class HistoryAdapterTest {
mHistoryProvider.addItem(item2);
initializeAdapter();
checkAdapterContents(true, null, null, item1, null, item2);
checkAdapterContents(mAdapter, true, false, null, null, item1, null, item2);
mAdapter.search("google");
// The header should be hidden during the search.
checkAdapterContents(false, null, item1);
checkAdapterContents(mAdapter, false, false, null, item1);
mAdapter.onEndSearch();
// The header should be shown again after the search.
checkAdapterContents(true, null, null, item1, null, item2);
checkAdapterContents(mAdapter, true, false, null, null, item1, null, item2);
}
@Test
......@@ -188,14 +197,15 @@ public class HistoryAdapterTest {
initializeAdapter();
// Only the first five of the seven items should be loaded.
checkAdapterContents(true, null, null, item1, item2, item3, item4, null, item5);
checkAdapterContents(
mAdapter, true, false, null, null, item1, item2, item3, item4, null, item5);
Assert.assertTrue(mAdapter.canLoadMoreItems());
mAdapter.loadMoreItems();
// All items should now be loaded.
checkAdapterContents(true, null, null, item1, item2, item3, item4, null, item5, item6,
null, item7);
checkAdapterContents(mAdapter, true, false, null, null, item1, item2, item3, item4, null,
item5, item6, null, item7);
Assert.assertFalse(mAdapter.canLoadMoreItems());
}
......@@ -209,13 +219,13 @@ public class HistoryAdapterTest {
initializeAdapter();
checkAdapterContents(true, null, null, item1);
checkAdapterContents(mAdapter, true, false, null, null, item1);
mHistoryProvider.removeItem(item1);
TestThreadUtils.runOnUiThreadBlocking(() -> mAdapter.onHistoryDeleted());
checkAdapterContents(false);
checkAdapterContents(mAdapter, false, false);
}
@Test
......@@ -231,30 +241,10 @@ public class HistoryAdapterTest {
initializeAdapter();
checkAdapterContents(true, null, null, item1, item2);
checkAdapterContents(mAdapter, true, false, null, null, item1, item2);
Assert.assertEquals(ContextUtils.getApplicationContext().getString(
R.string.android_history_blocked_site),
item2.getTitle());
Assert.assertTrue(item2.wasBlockedVisit());
}
private void checkAdapterContents(boolean hasHeader, Object... expectedItems) {
Assert.assertEquals(expectedItems.length, mAdapter.getItemCount());
Assert.assertEquals(hasHeader, mAdapter.hasListHeader());
for (int i = 0; i < expectedItems.length; i++) {
if (i == 0 && hasHeader) {
Assert.assertEquals(ItemViewType.HEADER, mAdapter.getItemViewType(i));
continue;
}
if (expectedItems[i] == null) {
// TODO(twellington): Check what date header is showing.
Assert.assertEquals(ItemViewType.DATE, mAdapter.getItemViewType(i));
} else {
Assert.assertEquals(ItemViewType.NORMAL, mAdapter.getItemViewType(i));
Assert.assertEquals(expectedItems[i], mAdapter.getItemAt(i).second);
}
}
}
}
// 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.
package org.chromium.chrome.browser.history;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.widget.RecyclerView;
import org.junit.Assert;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.chrome.browser.preferences.PrefChangeRegistrar.PrefObserver;
import org.chromium.chrome.browser.signin.SigninManager.SignInStateObserver;
import org.chromium.chrome.browser.widget.DateDividedAdapter.ItemViewType;
import org.chromium.chrome.browser.widget.selection.SelectionDelegate.SelectionObserver;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import java.util.List;
/**
* Util class for functions and helper classes that share between different test files.
*/
public class HistoryTestUtils {
/**
* Test Observer that used to collect the callback counts used in {@link HistoryActivityTest}
* {@link HistoryActivityScrollingTest}.
*/
static class TestObserver extends RecyclerView.AdapterDataObserver
implements SelectionObserver<HistoryItem>, SignInStateObserver, PrefObserver {
public final CallbackHelper onChangedCallback = new CallbackHelper();
public final CallbackHelper onSelectionCallback = new CallbackHelper();
public final CallbackHelper onSigninStateChangedCallback = new CallbackHelper();
public final CallbackHelper onPreferenceChangeCallback = new CallbackHelper();
private Handler mHandler;
public TestObserver() {
mHandler = new Handler(Looper.getMainLooper());
}
@Override
public void onChanged() {
// To guarantee that all real Observers have had a chance to react to the event, post
// the CallbackHelper.notifyCalled() call.
mHandler.post(() -> onChangedCallback.notifyCalled());
}
@Override
public void onSelectionStateChange(List<HistoryItem> selectedItems) {
mHandler.post(() -> onSelectionCallback.notifyCalled());
}
@Override
public void onSignedIn() {
mHandler.post(() -> onSigninStateChangedCallback.notifyCalled());
}
@Override
public void onSignedOut() {
mHandler.post(() -> onSigninStateChangedCallback.notifyCalled());
}
@Override
public void onPreferenceChange() {
mHandler.post(() -> onPreferenceChangeCallback.notifyCalled());
}
}
static void setupHistoryTestHeaders(HistoryAdapter adapter, TestObserver observer)
throws Exception {
if (!adapter.isClearBrowsingDataButtonVisible()) {
int changedCallCount = observer.onChangedCallback.getCallCount();
TestThreadUtils.runOnUiThreadBlocking(
() -> adapter.setClearBrowsingDataButtonVisibilityForTest(true));
observer.onChangedCallback.waitForCallback(changedCallCount);
}
if (adapter.arePrivacyDisclaimersVisible()) {
int changedCallCount = observer.onChangedCallback.getCallCount();
TestThreadUtils.runOnUiThreadBlocking(() -> adapter.hasOtherFormsOfBrowsingData(false));
observer.onChangedCallback.waitForCallback(changedCallCount);
}
}
static void checkAdapterContents(
HistoryAdapter adapter, boolean hasHeader, boolean hasFooter, Object... items) {
Assert.assertEquals(items.length, adapter.getItemCount());
Assert.assertEquals(hasHeader, adapter.hasListHeader());
Assert.assertEquals(hasFooter, adapter.hasListFooter());
for (int i = 0; i < items.length; i++) {
if (i == 0 && hasHeader) {
Assert.assertEquals(ItemViewType.HEADER, adapter.getItemViewType(i));
continue;
}
if (hasFooter && i == items.length - 1) {
Assert.assertEquals(ItemViewType.FOOTER, adapter.getItemViewType(i));
continue;
}
if (items[i] == null) {
// TODO(twellington): Check what date header is showing.
Assert.assertEquals(ItemViewType.DATE, adapter.getItemViewType(i));
} else {
Assert.assertEquals(ItemViewType.NORMAL, adapter.getItemViewType(i));
Assert.assertEquals(items[i], adapter.getItemAt(i).second);
}
}
}
}
......@@ -21,11 +21,13 @@ public class StubbedHistoryProvider implements HistoryProvider {
private BrowsingHistoryObserver mObserver;
private List<HistoryItem> mItems = new ArrayList<>();
private List<HistoryItem> mSearchItems = new ArrayList<>();
private List<HistoryItem> mRemovedItems = new ArrayList<>();
/** The exclusive end position for the last query. **/
private int mLastQueryEndPosition;
private String mLastQuery;
private int mPaging = 5;
@Override
public void setObserver(BrowsingHistoryObserver observer) {
......@@ -42,29 +44,31 @@ public class StubbedHistoryProvider implements HistoryProvider {
@Override
public void queryHistoryContinuation() {
// Simulate basic paging to facilitate testing loading more items.
// TODO(twellington): support loading more items while searching.
int queryStartPosition = mLastQueryEndPosition;
int queryStartPositionPlusFive = mLastQueryEndPosition + 5;
boolean hasMoreItems =
queryStartPositionPlusFive < mItems.size() && TextUtils.isEmpty(mLastQuery);
int queryEndPosition = hasMoreItems ? queryStartPositionPlusFive : mItems.size();
mLastQueryEndPosition = queryEndPosition;
List<HistoryItem> items = new ArrayList<>();
if (TextUtils.isEmpty(mLastQuery)) {
items = mItems.subList(queryStartPosition, queryEndPosition);
} else {
// Simulate basic search.
boolean isSearch = !TextUtils.isEmpty(mLastQuery);
if (!isSearch) {
mSearchItems.clear();
} else if (mLastQueryEndPosition == 0) {
// Start a new search; simulate basic search.
mLastQuery = mLastQuery.toLowerCase(Locale.getDefault());
for (HistoryItem item : mItems) {
if (item.getUrl().toLowerCase(Locale.getDefault()).contains(mLastQuery)
|| item.getTitle().toLowerCase(Locale.getDefault()).contains(mLastQuery)) {
items.add(item);
mSearchItems.add(item);
}
}
}
int queryStartPosition = mLastQueryEndPosition;
int queryStartPositionPlusPaging = mLastQueryEndPosition + mPaging;
List<HistoryItem> targetItems = isSearch ? mSearchItems : mItems;
boolean hasMoreItems = queryStartPositionPlusPaging < targetItems.size();
int queryEndPosition = hasMoreItems ? queryStartPositionPlusPaging : targetItems.size();
mLastQueryEndPosition = queryEndPosition;
List<HistoryItem> items = targetItems.subList(queryStartPosition, queryEndPosition);
mObserver.onQueryHistoryComplete(items, hasMoreItems);
}
......@@ -81,6 +85,7 @@ public class StubbedHistoryProvider implements HistoryProvider {
}
mRemovedItems.clear();
removeItemsCallback.notifyCalled();
mObserver.onHistoryDeleted();
}
@Override
......@@ -94,6 +99,14 @@ public class StubbedHistoryProvider implements HistoryProvider {
mItems.remove(item);
}
public void setPaging(int paging) {
mPaging = paging;
}
public int getPaging() {
return mPaging;
}
public static HistoryItem createHistoryItem(int which, long timestamp) {
long[] nativeTimestamps = {timestamp * 1000};
if (which == 0) {
......
......@@ -13,6 +13,8 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.chromium.base.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
......@@ -106,4 +108,9 @@ public class MoreProgressButton extends FrameLayout implements View.OnClickListe
this.mButton.setVisibility(State.BUTTON == state ? View.VISIBLE : View.GONE);
this.mProgressSpinner.setVisibility(State.LOADING == state ? View.VISIBLE : View.GONE);
}
@VisibleForTesting
public @State int getStateForTest() {
return mState;
}
}
......@@ -7,6 +7,7 @@ package org.chromium.chrome.browser.util;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Build;
import android.view.accessibility.AccessibilityManager;
......@@ -88,6 +89,13 @@ public class AccessibilityUtil {
sIsAccessibilityEnabled = null;
}
/**
* @return True if a hardware keyboard is detected.
*/
public static boolean isHardwareKeyboardAttached(Configuration c) {
return c.keyboard != Configuration.KEYBOARD_NOKEYS;
}
/**
* Checks whether the given {@link AccessibilityServiceInfo} can perform gestures.
* @param service The service to check.
......
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