| // Copyright 2018 The Feed Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.android.libraries.feed.basicstream; |
| |
| import static com.google.android.libraries.feed.api.stream.Stream.POSITION_NOT_KNOWN; |
| import static com.google.android.libraries.feed.basicstream.BasicStream.KEY_STREAM_STATE; |
| import static com.google.android.libraries.feed.basicstream.BasicStream.MINIMUM_SPINNER_SHOW_TIME; |
| import static com.google.android.libraries.feed.basicstream.BasicStream.MINIMUM_TIME_BEFORE_SHOWING_SPINNER; |
| import static com.google.android.libraries.feed.common.testing.RunnableSubject.assertThatRunnable; |
| import static com.google.common.truth.Truth.assertThat; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static org.mockito.Matchers.any; |
| import static org.mockito.Matchers.anyInt; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyNoMoreInteractions; |
| import static org.mockito.Mockito.when; |
| import static org.mockito.MockitoAnnotations.initMocks; |
| |
| import android.app.Activity; |
| import android.content.Context; |
| import android.os.Build.VERSION_CODES; |
| import android.os.Bundle; |
| import android.support.v7.widget.LinearLayoutManager; |
| import android.support.v7.widget.RecyclerView; |
| import android.util.Base64; |
| import android.view.View; |
| import android.widget.FrameLayout; |
| import com.google.android.libraries.feed.api.actionmanager.ActionManager; |
| import com.google.android.libraries.feed.api.actionparser.ActionParserFactory; |
| import com.google.android.libraries.feed.api.common.ThreadUtils; |
| import com.google.android.libraries.feed.api.knowncontent.ContentMetadata; |
| import com.google.android.libraries.feed.api.knowncontent.KnownContentApi; |
| import com.google.android.libraries.feed.api.modelprovider.ModelError; |
| import com.google.android.libraries.feed.api.modelprovider.ModelError.ErrorType; |
| import com.google.android.libraries.feed.api.modelprovider.ModelFeature; |
| import com.google.android.libraries.feed.api.modelprovider.ModelProvider; |
| import com.google.android.libraries.feed.api.modelprovider.ModelProvider.State; |
| import com.google.android.libraries.feed.api.modelprovider.ModelProvider.ViewDepthProvider; |
| import com.google.android.libraries.feed.api.modelprovider.ModelProviderFactory; |
| import com.google.android.libraries.feed.api.stream.ContentChangedListener; |
| import com.google.android.libraries.feed.api.stream.Header; |
| import com.google.android.libraries.feed.api.stream.ScrollListener; |
| import com.google.android.libraries.feed.basicstream.internal.StreamItemAnimator; |
| import com.google.android.libraries.feed.basicstream.internal.StreamRecyclerViewAdapter; |
| import com.google.android.libraries.feed.basicstream.internal.StreamSavedInstanceStateProto.StreamSavedInstanceState; |
| import com.google.android.libraries.feed.basicstream.internal.drivers.StreamDriver; |
| import com.google.android.libraries.feed.basicstream.internal.scroll.ScrollRestorer; |
| import com.google.android.libraries.feed.basicstream.internal.viewloggingupdater.ViewLoggingUpdater; |
| import com.google.android.libraries.feed.common.concurrent.MainThreadRunner; |
| import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner; |
| import com.google.android.libraries.feed.common.functional.Consumer; |
| import com.google.android.libraries.feed.common.time.Clock; |
| import com.google.android.libraries.feed.common.time.testing.FakeClock; |
| import com.google.android.libraries.feed.host.action.ActionApi; |
| import com.google.android.libraries.feed.host.config.Configuration; |
| import com.google.android.libraries.feed.host.config.Configuration.ConfigKey; |
| import com.google.android.libraries.feed.host.config.DebugBehavior; |
| import com.google.android.libraries.feed.host.imageloader.ImageLoaderApi; |
| import com.google.android.libraries.feed.host.logging.BasicLoggingApi; |
| import com.google.android.libraries.feed.host.logging.RequestReason; |
| import com.google.android.libraries.feed.host.logging.ZeroStateShowReason; |
| import com.google.android.libraries.feed.host.offlineindicator.OfflineIndicatorApi; |
| import com.google.android.libraries.feed.host.stream.CardConfiguration; |
| import com.google.android.libraries.feed.host.stream.SnackbarApi; |
| import com.google.android.libraries.feed.host.stream.StreamConfiguration; |
| import com.google.android.libraries.feed.host.stream.TooltipApi; |
| import com.google.android.libraries.feed.piet.PietManager; |
| import com.google.android.libraries.feed.piet.host.CustomElementProvider; |
| import com.google.android.libraries.feed.piet.host.HostBindingProvider; |
| import com.google.android.libraries.feed.sharedstream.contentchanged.StreamContentChangedListener; |
| import com.google.android.libraries.feed.sharedstream.contextmenumanager.ContextMenuManager; |
| import com.google.android.libraries.feed.sharedstream.contextmenumanager.ContextMenuManagerImpl; |
| import com.google.android.libraries.feed.sharedstream.deepestcontenttracker.DeepestContentTracker; |
| import com.google.android.libraries.feed.sharedstream.offlinemonitor.StreamOfflineMonitor; |
| import com.google.android.libraries.feed.sharedstream.piet.PietEventLogger; |
| import com.google.android.libraries.feed.sharedstream.proto.ScrollStateProto.ScrollState; |
| import com.google.android.libraries.feed.sharedstream.proto.UiRefreshReasonProto.UiRefreshReason; |
| import com.google.android.libraries.feed.sharedstream.proto.UiRefreshReasonProto.UiRefreshReason.Reason; |
| import com.google.android.libraries.feed.sharedstream.publicapi.menumeasurer.MenuMeasurer; |
| import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor; |
| import com.google.android.libraries.feed.testing.shadows.ShadowRecycledViewPool; |
| import com.google.protobuf.InvalidProtocolBufferException; |
| import com.google.search.now.feed.client.StreamDataProto.UiContext; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.robolectric.Robolectric; |
| import org.robolectric.RobolectricTestRunner; |
| import org.robolectric.annotation.Config; |
| import org.robolectric.shadow.api.Shadow; |
| |
| /** Tests for {@link BasicStream}. */ |
| @RunWith(RobolectricTestRunner.class) |
| @Config(shadows = {ShadowRecycledViewPool.class}) |
| public class BasicStreamTest { |
| |
| private static final int START_PADDING = 1; |
| private static final int END_PADDING = 2; |
| private static final int TOP_PADDING = 3; |
| private static final int BOTTOM_PADDING = 4; |
| private static final long LOGGING_IMMEDIATE_CONTENT_THRESHOLD_MS = 1000; |
| private static final int ADAPTER_HEADER_COUNT = 5; |
| |
| private static final String SESSION_ID = "session-id"; |
| private static final ScrollState SCROLL_STATE = |
| ScrollState.newBuilder().setOffset(10).setPosition(10).build(); |
| private static final StreamSavedInstanceState SAVED_INSTANCE_STATE = |
| StreamSavedInstanceState.newBuilder() |
| .setSessionId(SESSION_ID) |
| .setScrollState(SCROLL_STATE) |
| .build(); |
| private static final Configuration CONFIGURATION = |
| new Configuration.Builder() |
| .put( |
| ConfigKey.LOGGING_IMMEDIATE_CONTENT_THRESHOLD_MS, |
| LOGGING_IMMEDIATE_CONTENT_THRESHOLD_MS) |
| .build(); |
| |
| @Mock private StreamConfiguration streamConfiguration; |
| @Mock private ModelFeature modelFeature; |
| @Mock private ModelProviderFactory modelProviderFactory; |
| @Mock private ModelProvider initialModelProvider; |
| @Mock private ModelProvider modelProvider; |
| @Mock private ModelProvider restoredModelProvider; |
| @Mock private PietManager pietManager; |
| @Mock private SnackbarApi snackbarApi; |
| @Mock private StreamDriver streamDriver; |
| @Mock private StreamRecyclerViewAdapter adapter; |
| @Mock private StreamScrollMonitor streamScrollMonitor; |
| @Mock private ScrollRestorer nonRestoringScrollRestorer; |
| @Mock private ScrollRestorer scrollRestorer; |
| @Mock private BasicLoggingApi basicLoggingApi; |
| @Mock private ContextMenuManagerImpl contextMenuManager; |
| @Mock private ViewLoggingUpdater viewLoggingUpdater; |
| @Mock private TooltipApi tooltipApi; |
| |
| private FakeKnownContentApi knownContentApi; |
| private LinearLayoutManagerWithFakePositioning layoutManager; |
| private Context context; |
| private FakeClock clock; |
| private BasicStreamForTest basicStream; |
| private FakeMainThreadRunner mainThreadRunner; |
| private List<Header> headers; |
| |
| @Before |
| public void setUp() { |
| initMocks(this); |
| |
| knownContentApi = new FakeKnownContentApi(); |
| headers = new ArrayList<>(); |
| headers.add(mock(Header.class)); |
| |
| // TODO: Move header orchestration into separate class. |
| // Purposely using a different header count here as it is possible for size of headers to change |
| // due to swipe to dismiss. Adapter is source of truth for headers right now. Ideally we should |
| // have a drivers specifically for header management but we don't just yet. |
| when(adapter.getHeaderCount()).thenReturn(ADAPTER_HEADER_COUNT); |
| |
| when(streamConfiguration.getPaddingStart()).thenReturn(START_PADDING); |
| when(streamConfiguration.getPaddingEnd()).thenReturn(END_PADDING); |
| when(streamConfiguration.getPaddingTop()).thenReturn(TOP_PADDING); |
| when(streamConfiguration.getPaddingBottom()).thenReturn(BOTTOM_PADDING); |
| |
| when(modelProviderFactory.createNew(any(ViewDepthProvider.class))) |
| .thenReturn(initialModelProvider, modelProvider); |
| |
| when(initialModelProvider.getSessionId()).thenReturn(SESSION_ID); |
| |
| when(scrollRestorer.getScrollStateForScrollRestore(ADAPTER_HEADER_COUNT)) |
| .thenReturn(SCROLL_STATE); |
| |
| when(streamDriver.getLeafFeatureDrivers()).thenReturn(Collections.emptyList()); |
| |
| context = Robolectric.buildActivity(Activity.class).get(); |
| clock = new FakeClock(); |
| mainThreadRunner = FakeMainThreadRunner.create(clock); |
| layoutManager = new LinearLayoutManagerWithFakePositioning(context); |
| |
| basicStream = createBasicStream(layoutManager); |
| basicStream.onCreate((Bundle) null); |
| } |
| |
| @Test |
| public void testRecyclerViewSetup() { |
| assertThat(getStreamRecyclerView().getId()).isEqualTo(R.id.feed_stream_recycler_view); |
| } |
| |
| @Test |
| public void testOnSessionStart() { |
| basicStream.onShow(); |
| reset(adapter); |
| |
| basicStream.onSessionStart(); |
| |
| verify(streamDriver).onDestroy(); |
| verify(adapter).setDriver(streamDriver); |
| assertThat(basicStream.streamDriverScrollRestorer).isSameAs(scrollRestorer); |
| } |
| |
| @Test |
| public void testOnSessionStart_logsOnOpenedWithStreamContentAfterOnShow() { |
| clock = clock.set(10); |
| basicStream.onShow(); |
| |
| clock = clock.set(40); |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi).onOpenedWithContent(30, 0); |
| } |
| |
| @Test |
| public void testOnSessionStart_doesNotLogOnOpenedWithStreamContentAfterInitialOnShow() { |
| basicStream.onShow(); |
| basicStream.onSessionStart(); |
| reset(basicLoggingApi); |
| |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi, never()).onOpenedWithContent(anyInt(), anyInt()); |
| } |
| |
| @Test |
| public void testOnSessionStart_doesNotLogOnOpenedWithStreamContent_IfOnErrorLogsNoContent() { |
| basicStream.onShow(); |
| basicStream.onError(new ModelError(ErrorType.NO_CARDS_ERROR, /* continuationToken= */ null)); |
| reset(basicLoggingApi); |
| |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi, never()).onOpenedWithContent(anyInt(), anyInt()); |
| } |
| |
| @Test |
| public void |
| testOnSessionStart_doesNotUseNewStreamDriver_ifBothStreamDriversAreShowingZeroState() { |
| StreamDriver newStreamDriver = mock(StreamDriver.class); |
| basicStream.onShow(); |
| basicStream.onError(new ModelError(ErrorType.NO_CARDS_ERROR, /* continuationToken= */ null)); |
| reset(adapter, streamDriver); |
| when(streamDriver.isZeroStateBeingShown()).thenReturn(true); |
| when(newStreamDriver.isZeroStateBeingShown()).thenReturn(true); |
| |
| basicStream.streamDriver = newStreamDriver; |
| basicStream.onSessionStart(); |
| |
| verify(streamDriver).setModelProviderForZeroState(initialModelProvider); |
| verify(streamDriver, never()).onDestroy(); |
| verify(newStreamDriver).onDestroy(); |
| verify(adapter, never()).setDriver(any(StreamDriver.class)); |
| } |
| |
| @Test |
| public void testOnSessionFinished() { |
| basicStream.onShow(); |
| reset(streamDriver); |
| basicStream.onSessionFinished(UiContext.getDefaultInstance()); |
| |
| verify(scrollRestorer).abandonRestoringScroll(); |
| verify(initialModelProvider).unregisterObserver(basicStream); |
| verify(modelProviderFactory, times(2)).createNew(any(ViewDepthProvider.class)); |
| verify(modelProvider).registerObserver(basicStream); |
| } |
| |
| @Test |
| public void testOnError_showsZeroState() { |
| basicStream.onShow(); |
| reset(streamDriver); |
| |
| basicStream.onError(new ModelError(ErrorType.NO_CARDS_ERROR, /* continuationToken= */ null)); |
| |
| verify(scrollRestorer).abandonRestoringScroll(); |
| verify(streamDriver).showZeroState(ZeroStateShowReason.ERROR); |
| } |
| |
| @Test |
| public void testOnError_logsOnOpenedWithNoContent() { |
| when(streamDriver.hasContent()).thenReturn(false); |
| clock = clock.set(10); |
| basicStream.onShow(); |
| |
| clock = clock.set(10 + LOGGING_IMMEDIATE_CONTENT_THRESHOLD_MS); |
| basicStream.onError(new ModelError(ErrorType.NO_CARDS_ERROR, /* continuationToken= */ null)); |
| |
| verify(basicLoggingApi).onOpenedWithNoContent(); |
| } |
| |
| @Test |
| public void testOnError_doesNotDoubleLogOnOpenedWithNoContent() { |
| when(initialModelProvider.getCurrentState()).thenReturn(State.READY); |
| when(initialModelProvider.getRootFeature()).thenReturn(null); |
| |
| basicStream.onShow(); |
| // Trigger onOpenedWithNoContent logging through updating the driver. |
| basicStream.onSessionStart(); |
| reset(basicLoggingApi); |
| |
| basicStream.onError(new ModelError(ErrorType.NO_CARDS_ERROR, /* continuationToken= */ null)); |
| |
| verify(basicLoggingApi, never()).onOpenedWithNoContent(); |
| } |
| |
| @Test |
| public void testOnSessionStart_logsOnOpenedWithNoImmediateContent() { |
| basicStream.onShow(); |
| reset(streamDriver); |
| when(streamDriver.hasContent()).thenReturn(false); |
| |
| // Advance so that the spinner starts showing |
| clock.advance(MINIMUM_TIME_BEFORE_SHOWING_SPINNER); |
| |
| // Advance so that is has taken long enough that onOpenedWithNoImmediateContent is logged. |
| clock.advance(LOGGING_IMMEDIATE_CONTENT_THRESHOLD_MS); |
| |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi).onOpenedWithNoImmediateContent(); |
| } |
| |
| @Test |
| public void testOnSessionStart_logsOnOpenedWithNoContent() { |
| when(initialModelProvider.getCurrentState()).thenReturn(State.READY); |
| when(initialModelProvider.getRootFeature()).thenReturn(null); |
| basicStream.onShow(); |
| |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi).onOpenedWithNoContent(); |
| } |
| |
| @Test |
| public void testOnSessionStart_doesNotLogOnOpenedWithNoContent_ifModelProviderNotReady() { |
| when(initialModelProvider.getCurrentState()).thenReturn(State.INITIALIZING); |
| when(initialModelProvider.getRootFeature()).thenReturn(null); |
| basicStream.onShow(); |
| |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi, never()).onOpenedWithNoContent(); |
| } |
| |
| @Test |
| public void testOnSessionStart_doesNotLogOnOpenedWithNoContent_ifRootFeatureNotNull() { |
| when(initialModelProvider.getCurrentState()).thenReturn(State.READY); |
| when(initialModelProvider.getRootFeature()).thenReturn(modelFeature); |
| basicStream.onShow(); |
| |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi, never()).onOpenedWithNoContent(); |
| } |
| |
| @Test |
| public void testOnSessionStart_doesNotDoubleLogOnOpenedWithNoContent() { |
| when(initialModelProvider.getCurrentState()).thenReturn(State.READY); |
| when(initialModelProvider.getRootFeature()).thenReturn(null); |
| |
| basicStream.onShow(); |
| // Trigger onOpenedWithNoContent logging through onError. |
| basicStream.onError(new ModelError(ErrorType.NO_CARDS_ERROR, /* continuationToken= */ null)); |
| reset(basicLoggingApi); |
| |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi, never()).onOpenedWithNoContent(); |
| } |
| |
| @Test |
| public void testOnSessionStart_doesNotLogOnOpenedWithNoImmediateContentAfterInitialOnShow() { |
| basicStream.onShow(); |
| basicStream.onSessionStart(); |
| reset(basicLoggingApi); |
| |
| basicStream.onSessionStart(); |
| |
| verify(basicLoggingApi, never()).onOpenedWithNoImmediateContent(); |
| } |
| |
| @Test |
| public void testOnError_doesNotShowZeroState() { |
| basicStream.onShow(); |
| |
| assertThatRunnable( |
| () -> |
| basicStream.onError( |
| new ModelError(ErrorType.PAGINATION_ERROR, /* continuationToken= */ null))) |
| .throwsAnExceptionOfType(RuntimeException.class); |
| } |
| |
| @Test |
| public void testLifecycle_onCreateWithBundleCalledOnlyOnce() { |
| // onCreate is called once in setup |
| assertThatRunnable(() -> basicStream.onCreate(new Bundle())) |
| .throwsAnExceptionOfType(IllegalStateException.class); |
| } |
| |
| @Test |
| public void testLifecycle_onCreateWithStringCalledOnlyOnce() { |
| // onCreate is called once in setup |
| assertThatRunnable(() -> basicStream.onCreate("")) |
| .throwsAnExceptionOfType(IllegalStateException.class); |
| } |
| |
| @Test |
| public void testLifecycle_getViewBeforeOnCreateCrashes() { |
| // create BasicStream that has not had onCreate() called. |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| assertThatRunnable(() -> basicStream.getView()) |
| .throwsAnExceptionOfType(IllegalStateException.class); |
| } |
| |
| @Test |
| public void testLifecycle_onCreate_onDestroy() { |
| basicStream.onDestroy(); |
| verify(initialModelProvider, never()).invalidate(); |
| } |
| |
| @Test |
| public void testLifecycle_onCreate_onShow_onHide_onDestroy() { |
| basicStream.onShow(); |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| verify(adapter).onDestroy(); |
| verify(initialModelProvider, never()).invalidate(); |
| verify(initialModelProvider).detachModelProvider(); |
| assertThat(knownContentApi.listeners).isEmpty(); |
| } |
| |
| @Test |
| public void testOnDestroy_destroysStreamDriver() { |
| basicStream.onShow(); |
| basicStream.onSessionStart(); |
| reset(streamDriver); |
| |
| basicStream.onDestroy(); |
| |
| verify(streamDriver).onDestroy(); |
| } |
| |
| @Test |
| public void testOnDestroy_unregistersOnLayoutChangeListener() { |
| // initial layout from 0, 0 will not rebind. |
| getStreamRecyclerView().layout(0, 0, 100, 300); |
| // change the width / height to simulate device rotation |
| getStreamRecyclerView().layout(0, 0, 300, 100); |
| verify(adapter).rebind(); |
| |
| reset(adapter); |
| basicStream.onDestroy(); |
| |
| // change the width / height to simulate device rotation |
| getStreamRecyclerView().layout(0, 0, 100, 300); |
| verify(adapter, never()).rebind(); |
| } |
| |
| @Test |
| public void testOnDestroy_deregistersSessionListener() { |
| basicStream.onShow(); |
| |
| basicStream.onDestroy(); |
| |
| // Once for BasicStream, once for the session listener. |
| verify(initialModelProvider, times(2)).unregisterObserver(any()); |
| } |
| |
| @Test |
| public void testGetSavedInstanceState() { |
| basicStream.onShow(); |
| |
| Bundle bundle = basicStream.getSavedInstanceState(); |
| assertThat(bundle.getString(KEY_STREAM_STATE)) |
| .isEqualTo(Base64.encodeToString(SAVED_INSTANCE_STATE.toByteArray(), Base64.DEFAULT)); |
| } |
| |
| @Test |
| public void testGetSavedInstanceStateString_beforeShow() throws InvalidProtocolBufferException { |
| StreamSavedInstanceState savedInstanceState = |
| StreamSavedInstanceState.parseFrom( |
| decodeSavedInstanceStateString(basicStream.getSavedInstanceStateString())); |
| assertThat(savedInstanceState.hasSessionId()).isFalse(); |
| assertThat(savedInstanceState.getScrollState()).isEqualTo(SCROLL_STATE); |
| } |
| |
| @Test |
| public void testGetSavedInstanceStateString_afterOnShow_beforeSessionStart() |
| throws InvalidProtocolBufferException { |
| when(initialModelProvider.getSessionId()).thenReturn(null); |
| |
| basicStream.onShow(); |
| |
| StreamSavedInstanceState savedInstanceState = |
| StreamSavedInstanceState.parseFrom( |
| decodeSavedInstanceStateString(basicStream.getSavedInstanceStateString())); |
| assertThat(savedInstanceState.hasSessionId()).isFalse(); |
| assertThat(savedInstanceState.getScrollState()).isEqualTo(SCROLL_STATE); |
| } |
| |
| @Test |
| public void testGetSavedInstanceStateString_afterOnShow_afterSessionStart() |
| throws InvalidProtocolBufferException { |
| basicStream.onShow(); |
| |
| StreamSavedInstanceState savedInstanceState = |
| StreamSavedInstanceState.parseFrom( |
| decodeSavedInstanceStateString(basicStream.getSavedInstanceStateString())); |
| assertThat(savedInstanceState.getSessionId()).isEqualTo(SESSION_ID); |
| assertThat(savedInstanceState.getScrollState()).isEqualTo(SCROLL_STATE); |
| } |
| |
| @Test |
| public void testGetSavedInstanceStateString_noScrollRestoreBundle() |
| throws InvalidProtocolBufferException { |
| basicStream.onShow(); |
| |
| when(scrollRestorer.getScrollStateForScrollRestore(ADAPTER_HEADER_COUNT)).thenReturn(null); |
| StreamSavedInstanceState savedInstanceState = |
| StreamSavedInstanceState.parseFrom( |
| decodeSavedInstanceStateString(basicStream.getSavedInstanceStateString())); |
| assertThat(savedInstanceState.hasScrollState()).isFalse(); |
| } |
| |
| @Test |
| public void testRestore() { |
| basicStream.onShow(); |
| |
| Bundle bundle = basicStream.getSavedInstanceState(); |
| |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| |
| when(modelProviderFactory.create(SESSION_ID)).thenReturn(restoredModelProvider); |
| |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| basicStream.onCreate(bundle); |
| |
| basicStream.onShow(); |
| |
| verify(restoredModelProvider).registerObserver(basicStream); |
| } |
| |
| @Test |
| public void testRestore_withStringSavedState() { |
| basicStream.onShow(); |
| |
| String savedInstanceState = basicStream.getSavedInstanceStateString(); |
| |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| |
| when(modelProviderFactory.create(SESSION_ID)).thenReturn(restoredModelProvider); |
| |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| basicStream.onCreate(savedInstanceState); |
| |
| basicStream.onShow(); |
| |
| verify(restoredModelProvider).registerObserver(basicStream); |
| } |
| |
| @Test |
| public void testRestore_doesNotShowZeroState() { |
| basicStream.onShow(); |
| |
| Bundle bundle = basicStream.getSavedInstanceState(); |
| |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| |
| when(modelProviderFactory.create(SESSION_ID)).thenReturn(restoredModelProvider); |
| |
| reset(streamDriver); |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| basicStream.onCreate(bundle); |
| |
| basicStream.onShow(); |
| |
| verify(streamDriver, never()).showZeroState(/* zeroStateShowReason= */ anyInt()); |
| verify(streamDriver, never()).showSpinner(); |
| } |
| |
| @Test |
| public void testRestore_showsZeroStateIfNoSessionToRestore() { |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| basicStream.onCreate(Bundle.EMPTY); |
| |
| basicStream.onShow(); |
| |
| verify(streamDriver, never()).showSpinner(); |
| |
| clock.advance(MINIMUM_TIME_BEFORE_SHOWING_SPINNER); |
| |
| verify(streamDriver).showSpinner(); |
| } |
| |
| @Test |
| public void testRestore_invalidSession() { |
| basicStream.onShow(); |
| |
| Bundle bundle = basicStream.getSavedInstanceState(); |
| |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| basicStream.onCreate(bundle); |
| basicStream.onShow(); |
| |
| verify(modelProvider).registerObserver(basicStream); |
| } |
| |
| @Test |
| public void testRestore_invalidBase64Encoding() { |
| basicStream.onShow(); |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString(KEY_STREAM_STATE, "=invalid"); |
| |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| assertThatRunnable(() -> basicStream.onCreate(bundle)) |
| .throwsAnExceptionOfType(RuntimeException.class); |
| } |
| |
| @Test |
| public void testRestore_invalidProtocolBuffer() { |
| basicStream.onShow(); |
| |
| Bundle bundle = new Bundle(); |
| bundle.putString( |
| KEY_STREAM_STATE, Base64.encodeToString("invalid".getBytes(UTF_8), Base64.DEFAULT)); |
| |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| assertThatRunnable(() -> basicStream.onCreate(bundle)) |
| .throwsAnExceptionOfType(RuntimeException.class); |
| } |
| |
| @Test |
| public void testRestore_createsStreamDriver() { |
| basicStream.onShow(); |
| |
| Bundle bundle = basicStream.getSavedInstanceState(); |
| |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| |
| when(modelProviderFactory.create(SESSION_ID)).thenReturn(restoredModelProvider); |
| |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| basicStream.onCreate(bundle); |
| |
| basicStream.onShow(); |
| |
| assertThat(basicStream.streamDriverRestoring).isTrue(); |
| } |
| |
| @Test |
| public void testRestore_createsStreamDriver_afterFailure() { |
| basicStream.onShow(); |
| |
| Bundle bundle = basicStream.getSavedInstanceState(); |
| |
| basicStream.onHide(); |
| basicStream.onDestroy(); |
| |
| when(modelProviderFactory.create(SESSION_ID)).thenReturn(restoredModelProvider); |
| |
| basicStream = createBasicStream(new LinearLayoutManagerWithFakePositioning(context)); |
| basicStream.onCreate(bundle); |
| |
| // onSessionFinish indicates the restore has failed. |
| basicStream.onSessionFinished(UiContext.getDefaultInstance()); |
| |
| basicStream.onShow(); |
| |
| assertThat(basicStream.streamDriverRestoring).isFalse(); |
| } |
| |
| @Test |
| @Config(sdk = VERSION_CODES.JELLY_BEAN) |
| public void testPadding_jellyBean() { |
| // Padding is setup in constructor. |
| View view = basicStream.getView(); |
| |
| assertThat(view.getPaddingLeft()).isEqualTo(START_PADDING); |
| assertThat(view.getPaddingRight()).isEqualTo(END_PADDING); |
| assertThat(view.getPaddingTop()).isEqualTo(TOP_PADDING); |
| assertThat(view.getPaddingBottom()).isEqualTo(BOTTOM_PADDING); |
| } |
| |
| @Test |
| @Config(sdk = VERSION_CODES.KITKAT) |
| public void testPadding_kitKat() { |
| // Padding is setup in constructor. |
| View view = basicStream.getView(); |
| |
| assertThat(view.getPaddingStart()).isEqualTo(START_PADDING); |
| assertThat(view.getPaddingEnd()).isEqualTo(END_PADDING); |
| assertThat(view.getPaddingTop()).isEqualTo(TOP_PADDING); |
| assertThat(view.getPaddingBottom()).isEqualTo(BOTTOM_PADDING); |
| } |
| |
| @Test |
| public void testTrim() { |
| ShadowRecycledViewPool viewPool = Shadow.extract(getStreamRecyclerView().getRecycledViewPool()); |
| |
| // RecyclerView ends up clearing the pool initially when the adapter is set on the RecyclerView. |
| // Verify that has happened before anything else |
| assertThat(viewPool.getClearCallCount()).isEqualTo(1); |
| |
| basicStream.trim(); |
| |
| verify(pietManager).purgeRecyclerPools(); |
| |
| // We expect the clear() call to be 2 as RecyclerView ends up clearing the pool initially when |
| // the adapter is set on the RecyclerView. So one call for that and one call for trim() call |
| // on stream. |
| assertThat(viewPool.getClearCallCount()).isEqualTo(2); |
| } |
| |
| @Test |
| public void testLayoutChanges() { |
| // initial layout from 0, 0 will not rebind. |
| getStreamRecyclerView().layout(0, 0, 100, 300); |
| verify(adapter, never()).rebind(); |
| // change the width / height to simulate device rotation |
| getStreamRecyclerView().layout(0, 0, 300, 100); |
| verify(adapter).rebind(); |
| } |
| |
| @Test |
| public void testAddScrollListener() { |
| ScrollListener scrollListener = mock(ScrollListener.class); |
| basicStream.addScrollListener(scrollListener); |
| verify(streamScrollMonitor).addScrollListener(scrollListener); |
| } |
| |
| @Test |
| public void testRemoveScrollListener() { |
| ScrollListener scrollListener = mock(ScrollListener.class); |
| basicStream.removeScrollListener(scrollListener); |
| verify(streamScrollMonitor).removeScrollListener(scrollListener); |
| } |
| |
| @Test |
| public void testIsChildAtPositionVisible() { |
| layoutManager.firstVisiblePosition = 0; |
| layoutManager.lastVisiblePosition = 1; |
| assertThat(basicStream.isChildAtPositionVisible(-2)).isFalse(); |
| assertThat(basicStream.isChildAtPositionVisible(-1)).isFalse(); |
| assertThat(basicStream.isChildAtPositionVisible(0)).isTrue(); |
| assertThat(basicStream.isChildAtPositionVisible(1)).isTrue(); |
| assertThat(basicStream.isChildAtPositionVisible(2)).isFalse(); |
| } |
| |
| @Test |
| public void testIsChildAtPositionVisible_nothingVisible() { |
| assertThat(basicStream.isChildAtPositionVisible(0)).isFalse(); |
| } |
| |
| @Test |
| public void testIsChildAtPositionVisible_validTop() { |
| layoutManager.firstVisiblePosition = 0; |
| assertThat(basicStream.isChildAtPositionVisible(0)).isFalse(); |
| } |
| |
| @Test |
| public void testIsChildAtPositionVisible_validBottom() { |
| layoutManager.lastVisiblePosition = 1; |
| assertThat(basicStream.isChildAtPositionVisible(0)).isFalse(); |
| } |
| |
| @Test |
| public void testGetChildTopAt_noVisibleChild() { |
| assertThat(basicStream.getChildTopAt(0)).isEqualTo(POSITION_NOT_KNOWN); |
| } |
| |
| @Test |
| public void testGetChildTopAt_noChild() { |
| layoutManager.firstVisiblePosition = 0; |
| layoutManager.lastVisiblePosition = 1; |
| assertThat(basicStream.getChildTopAt(0)).isEqualTo(POSITION_NOT_KNOWN); |
| } |
| |
| @Test |
| public void testGetChildTopAt() { |
| layoutManager.firstVisiblePosition = 0; |
| layoutManager.lastVisiblePosition = 1; |
| View view = new FrameLayout(context); |
| layoutManager.addChildToPosition(0, view); |
| |
| assertThat(basicStream.getChildTopAt(0)).isEqualTo(view.getTop()); |
| } |
| |
| @Test |
| public void testStreamContentVisible() { |
| basicStream.setStreamContentVisibility(false); |
| verify(adapter).setStreamContentVisible(false); |
| |
| reset(adapter); |
| basicStream.setStreamContentVisibility(true); |
| verify(adapter).setStreamContentVisible(true); |
| } |
| |
| @Test |
| public void testStreamContentVisible_notifiesItemAnimator_notVisible() { |
| basicStream.setStreamContentVisibility(false); |
| |
| assertThat( |
| ((StreamItemAnimator) getStreamRecyclerView().getItemAnimator()) |
| .getStreamContentVisibility()) |
| .isFalse(); |
| } |
| |
| @Test |
| public void testStreamContentVisible_notifiesItemAnimator_visible() { |
| basicStream.setStreamContentVisibility(true); |
| |
| assertThat( |
| ((StreamItemAnimator) getStreamRecyclerView().getItemAnimator()) |
| .getStreamContentVisibility()) |
| .isTrue(); |
| } |
| |
| @Test |
| public void testSetStreamContentVisibility_createsModelProvider_ifContentNowVisible() { |
| basicStream.setStreamContentVisibility(false); |
| basicStream.onShow(); |
| |
| verifyNoMoreInteractions(modelProvider); |
| |
| basicStream.setStreamContentVisibility(true); |
| |
| verify(modelProviderFactory).createNew(any(ViewDepthProvider.class)); |
| } |
| |
| @Test |
| public void testSetStreamContentVisibility_resetsViewLogging() { |
| basicStream.setStreamContentVisibility(false); |
| basicStream.onShow(); |
| |
| basicStream.setStreamContentVisibility(true); |
| |
| verify(viewLoggingUpdater).resetViewTracking(); |
| } |
| |
| @Test |
| public void testSetStreamContentVisibility_trueMultipleTimes_doesNotResetViewLogging() { |
| basicStream.setStreamContentVisibility(true); |
| basicStream.onShow(); |
| |
| basicStream.setStreamContentVisibility(true); |
| |
| verify(viewLoggingUpdater, never()).resetViewTracking(); |
| } |
| |
| @Test |
| public void testTriggerRefresh() { |
| basicStream.onShow(); |
| |
| reset(adapter, streamDriver); |
| |
| basicStream.triggerRefresh(); |
| |
| verify(initialModelProvider).triggerRefresh(RequestReason.HOST_REQUESTED); |
| verify(streamDriver).showSpinner(); |
| } |
| |
| @Test |
| public void testTriggerRefresh_beforeOnShow() { |
| basicStream.triggerRefresh(); |
| |
| verify(initialModelProvider, never()).triggerRefresh(anyInt()); |
| verify(streamDriver, never()).showSpinner(); |
| } |
| |
| @Test |
| public void testAddOnContentChangedListener() { |
| ContentChangedListener contentChangedListener = mock(ContentChangedListener.class); |
| |
| basicStream.addOnContentChangedListener(contentChangedListener); |
| basicStream.contentChangedListener.onContentChanged(); |
| |
| verify(contentChangedListener).onContentChanged(); |
| } |
| |
| @Test |
| public void testRemoveOnContentChangedListener() { |
| ContentChangedListener contentChangedListener = mock(ContentChangedListener.class); |
| |
| basicStream.addOnContentChangedListener(contentChangedListener); |
| basicStream.removeOnContentChangedListener(contentChangedListener); |
| |
| basicStream.contentChangedListener.onContentChanged(); |
| |
| verify(contentChangedListener, never()).onContentChanged(); |
| } |
| |
| @Test |
| public void testOnShow_nonRestoringRestorer() { |
| basicStream.onShow(); |
| |
| assertThat(basicStream.streamDriverScrollRestorer).isEqualTo(nonRestoringScrollRestorer); |
| } |
| |
| @Test |
| public void testOnShow_restoringScrollRestorer() { |
| when(initialModelProvider.getCurrentState()).thenReturn(State.READY); |
| basicStream.onShow(); |
| |
| assertThat(basicStream.streamDriverScrollRestorer).isEqualTo(scrollRestorer); |
| } |
| |
| @Test |
| public void testOnShow_setShown() { |
| basicStream.onShow(); |
| |
| verify(adapter).setShown(true); |
| } |
| |
| @Test |
| public void testOnShow_doesNotCreateModelProvider_ifStreamContentNotVisible() { |
| basicStream.setStreamContentVisibility(false); |
| |
| basicStream.onShow(); |
| |
| verifyNoMoreInteractions(modelProviderFactory); |
| verify(adapter, never()).setDriver(any(StreamDriver.class)); |
| } |
| |
| @Test |
| public void testOnShow_restoresScrollPosition_ifStreamContentNotVisible() { |
| basicStream.setStreamContentVisibility(false); |
| |
| basicStream.onShow(); |
| |
| verify(scrollRestorer).maybeRestoreScroll(); |
| } |
| |
| @Test |
| public void testOnSessionStart_showsContentImmediately_ifNotInitialLoad() { |
| basicStream.onShow(); |
| basicStream.onSessionStart(); |
| reset(adapter); |
| |
| basicStream.onSessionStart(); |
| verify(adapter).setDriver(streamDriver); |
| } |
| |
| @Test |
| public void testOnSessionStart_showsContentImmediately_ifSpinnerTimeElapsed() { |
| basicStream.onShow(); |
| reset(adapter); |
| |
| // Advance so the spinner is shown. |
| clock.advance(MINIMUM_TIME_BEFORE_SHOWING_SPINNER); |
| |
| // Advance so that it has shown long enough |
| clock.advance(MINIMUM_SPINNER_SHOW_TIME); |
| |
| basicStream.onSessionStart(); |
| |
| // The adapter should be immediately set in onSesssionStart |
| verify(adapter).setDriver(streamDriver); |
| } |
| |
| @Test |
| public void testOnSessionStart_showsContentWithDelay_ifSpinnerTimeNotElapsed() { |
| basicStream.onShow(); |
| |
| // Advance so the spinner is shown. |
| clock.advance(MINIMUM_TIME_BEFORE_SHOWING_SPINNER); |
| |
| reset(adapter); |
| |
| basicStream.onSessionStart(); |
| verify(adapter, never()).setDriver(any(StreamDriver.class)); |
| |
| // Advance so that it has shown long enough. |
| clock.advance(MINIMUM_SPINNER_SHOW_TIME); |
| |
| verify(adapter).setDriver(streamDriver); |
| } |
| |
| @Test |
| public void testOnSessionStart_doesNotShowContent_ifSessionFinishesBeforeSpinnerTimeElapsed() { |
| clock.set(10); |
| |
| basicStream.onShow(); |
| |
| reset(adapter); |
| |
| // Advance so the spinner is shown. |
| clock.advance(MINIMUM_TIME_BEFORE_SHOWING_SPINNER); |
| |
| basicStream.onSessionStart(); |
| basicStream.onSessionFinished(UiContext.getDefaultInstance()); |
| |
| // Advance so that it has shown long enough. |
| clock.advance(MINIMUM_SPINNER_SHOW_TIME); |
| |
| verify(adapter, never()).setDriver(streamDriver); |
| } |
| |
| @Test |
| public void testOnSessionStart_showsZeroState_ifFeatureDriversEmpty() { |
| basicStream.onShow(); |
| |
| basicStream.onSessionStart(); |
| |
| verify(streamDriver).showZeroState(ZeroStateShowReason.NO_CONTENT); |
| } |
| |
| @Test |
| public void testOnShow_delaysShowingZeroState_onInitialLoad() { |
| basicStream.onShow(); |
| |
| verify(streamDriver, never()).showZeroState(/* zeroStateShowReason= */ anyInt()); |
| verify(streamDriver, never()).showSpinner(); |
| |
| // Advance so the spinner is shown. |
| clock.advance(MINIMUM_TIME_BEFORE_SHOWING_SPINNER); |
| |
| verify(streamDriver).showSpinner(); |
| } |
| |
| @Test |
| public void testOnHide_setShown() { |
| basicStream.onShow(); |
| reset(adapter); |
| |
| basicStream.onHide(); |
| |
| verify(adapter).setShown(false); |
| } |
| |
| @Test |
| public void testOnHide_dismissesPopup() { |
| basicStream.onHide(); |
| verify(contextMenuManager).dismissPopup(); |
| } |
| |
| @Test |
| public void onSessionFinished_afterOnDestroy_unregistersOnce() { |
| basicStream.onShow(); |
| |
| basicStream.onDestroy(); |
| basicStream.onSessionFinished(UiContext.getDefaultInstance()); |
| |
| // Should only occur once between onDestroy() and onSessionFinished() |
| verify(initialModelProvider).unregisterObserver(basicStream); |
| } |
| |
| @Test |
| public void onDestroyTwice_unregistersOnce() { |
| basicStream.onShow(); |
| |
| basicStream.onDestroy(); |
| basicStream.onDestroy(); |
| |
| // Should only occur once between multiple onDestroy() calls |
| verify(initialModelProvider).unregisterObserver(basicStream); |
| } |
| |
| @Test |
| public void onSessionStart_propagatesUiContext() { |
| basicStream.onShow(); |
| |
| UiRefreshReason uiRefreshReason = |
| UiRefreshReason.newBuilder().setReason(Reason.ZERO_STATE).build(); |
| basicStream.onSessionStart(); |
| basicStream.onSessionFinished( |
| UiContext.newBuilder() |
| .setExtension(UiRefreshReason.uiRefreshReasonExtension, uiRefreshReason) |
| .build()); |
| basicStream.onSessionStart(); |
| |
| assertThat(basicStream.streamDriverUiRefreshReason).isEqualTo(uiRefreshReason); |
| } |
| |
| @Test |
| public void onShow_whileStreamContentNotVisible_logsOpenedWithNoContent() { |
| basicStream.setStreamContentVisibility(false); |
| |
| basicStream.onShow(); |
| |
| verify(basicLoggingApi).onOpenedWithNoContent(); |
| } |
| |
| @Test |
| public void onShow_whileStreamContentNotVisible_twice_logsOpenedWithNoContentOnce() { |
| basicStream.setStreamContentVisibility(false); |
| |
| basicStream.onShow(); |
| basicStream.onHide(); |
| basicStream.onShow(); |
| |
| verify(basicLoggingApi).onOpenedWithNoContent(); |
| } |
| |
| @Test |
| public void failToRestoreSession_doesntShowSpinnerImmediately() { |
| basicStream.onShow(); |
| |
| basicStream.onSessionFinished(UiContext.getDefaultInstance()); |
| |
| verify(streamDriver, never()).showSpinner(); |
| } |
| |
| @Test |
| public void failToRestoreSession_showsSpinnerAfterDelay() { |
| basicStream.onShow(); |
| |
| basicStream.onSessionFinished(UiContext.getDefaultInstance()); |
| |
| clock.advance(BasicStream.MINIMUM_TIME_BEFORE_SHOWING_SPINNER); |
| |
| verify(streamDriver).showSpinner(); |
| } |
| |
| @Test |
| public void startSession_quicklyFinish_doesntShowInitialSpinner() { |
| basicStream.onShow(); |
| |
| // Advance, but not enough so that the spinner shows. |
| clock.advance(MINIMUM_TIME_BEFORE_SHOWING_SPINNER / 2); |
| |
| // Start the session. At this point, the spinner enqueued by onShow() should never be shown. |
| basicStream.onSessionStart(); |
| |
| // Finish the session immediately. As we had started a session and the user had presumably seen |
| // content, we shouldn't trigger the spinner enqueued by onShow(). |
| basicStream.onSessionFinished(UiContext.getDefaultInstance()); |
| |
| // It has now been long enough since onShow that the spinner would show if onSessionStart() |
| // hadn't been called first. |
| clock.advance(BasicStream.MINIMUM_TIME_BEFORE_SHOWING_SPINNER / 2); |
| |
| verify(streamDriver, never()).showSpinner(); |
| } |
| |
| @Test |
| public void startSession_thenFinish_showsSpinnerAfterDelay() { |
| basicStream.onShow(); |
| |
| basicStream.onSessionStart(); |
| basicStream.onSessionFinished(UiContext.getDefaultInstance()); |
| |
| verify(streamDriver, never()).showSpinner(); |
| |
| clock.advance(MINIMUM_TIME_BEFORE_SHOWING_SPINNER); |
| |
| verify(streamDriver).showSpinner(); |
| } |
| |
| private byte[] decodeSavedInstanceStateString(String savedInstanceState) { |
| return Base64.decode(savedInstanceState, Base64.DEFAULT); |
| } |
| |
| private RecyclerView getStreamRecyclerView() { |
| return (RecyclerView) basicStream.getView(); |
| } |
| |
| private BasicStreamForTest createBasicStream(LinearLayoutManager layoutManager) { |
| |
| return new BasicStreamForTest( |
| context, |
| streamConfiguration, |
| mock(CardConfiguration.class), |
| mock(ImageLoaderApi.class), |
| mock(ActionParserFactory.class), |
| mock(ActionApi.class), |
| mock(CustomElementProvider.class), |
| DebugBehavior.VERBOSE, |
| new ThreadUtils(), |
| headers, |
| clock, |
| modelProviderFactory, |
| new HostBindingProvider(), |
| mock(ActionManager.class), |
| CONFIGURATION, |
| layoutManager, |
| mock(OfflineIndicatorApi.class), |
| streamDriver); |
| } |
| |
| private class BasicStreamForTest extends BasicStream { |
| |
| private final LinearLayoutManager layoutManager; |
| private StreamDriver streamDriver; |
| private boolean streamDriverRestoring; |
| |
| private ScrollRestorer streamDriverScrollRestorer; |
| private StreamContentChangedListener contentChangedListener; |
| private UiRefreshReason streamDriverUiRefreshReason; |
| |
| public BasicStreamForTest( |
| Context context, |
| StreamConfiguration streamConfiguration, |
| CardConfiguration cardConfiguration, |
| ImageLoaderApi imageLoaderApi, |
| ActionParserFactory actionParserFactory, |
| ActionApi actionApi, |
| /*@Nullable*/ CustomElementProvider customElementProvider, |
| DebugBehavior debugBehavior, |
| ThreadUtils threadUtils, |
| List<Header> headers, |
| Clock clock, |
| ModelProviderFactory modelProviderFactory, |
| /*@Nullable*/ HostBindingProvider hostBindingProvider, |
| ActionManager actionManager, |
| Configuration configuration, |
| LinearLayoutManager layoutManager, |
| OfflineIndicatorApi offlineIndicatorApi, |
| StreamDriver streamDriver) { |
| super( |
| context, |
| streamConfiguration, |
| cardConfiguration, |
| imageLoaderApi, |
| actionParserFactory, |
| actionApi, |
| customElementProvider, |
| debugBehavior, |
| threadUtils, |
| headers, |
| clock, |
| modelProviderFactory, |
| hostBindingProvider, |
| actionManager, |
| configuration, |
| snackbarApi, |
| basicLoggingApi, |
| offlineIndicatorApi, |
| mainThreadRunner, |
| knownContentApi, |
| tooltipApi, |
| /* isBackgroundDark= */ false); |
| this.layoutManager = layoutManager; |
| this.streamDriver = streamDriver; |
| } |
| |
| @Override |
| PietManager createPietManager( |
| Context context, |
| CardConfiguration cardConfiguration, |
| ImageLoaderApi imageLoaderApi, |
| /*@Nullable*/ CustomElementProvider customElementProvider, |
| DebugBehavior debugBehavior, |
| Clock clock, |
| /*@Nullable*/ HostBindingProvider hostBindingProvider, |
| StreamOfflineMonitor streamOfflineMonitor, |
| Configuration config, |
| boolean isBackgroundDark) { |
| return pietManager; |
| } |
| |
| @Override |
| StreamDriver createStreamDriver( |
| ActionApi actionApi, |
| ActionManager actionManager, |
| ActionParserFactory actionParserFactory, |
| ModelProvider modelProvider, |
| ThreadUtils threadUtils, |
| Clock clock, |
| Configuration configuration, |
| Context context, |
| SnackbarApi snackbarApi, |
| ContentChangedListener contentChangedListener, |
| ScrollRestorer scrollRestorer, |
| BasicLoggingApi basicLoggingApi, |
| StreamOfflineMonitor streamOfflineMonitor, |
| KnownContentApi knownContentApi, |
| ContextMenuManager contextMenuManager, |
| boolean restoring, |
| boolean isInitialLoad, |
| MainThreadRunner mainThreadRunner, |
| TooltipApi tooltipApi, |
| UiRefreshReason uiRefreshReason, |
| StreamScrollMonitor streamScrollMonitor) { |
| streamDriverScrollRestorer = scrollRestorer; |
| streamDriverRestoring = restoring; |
| this.streamDriverUiRefreshReason = uiRefreshReason; |
| return streamDriver; |
| } |
| |
| @Override |
| StreamContentChangedListener createStreamContentChangedListener() { |
| contentChangedListener = new StreamContentChangedListener(); |
| return contentChangedListener; |
| } |
| |
| @Override |
| StreamRecyclerViewAdapter createRecyclerViewAdapter( |
| Context context, |
| CardConfiguration cardConfiguration, |
| PietManager pietManager, |
| DeepestContentTracker deepestContentTracker, |
| StreamContentChangedListener streamContentChangedListener, |
| StreamScrollMonitor streamScrollMonitor, |
| Configuration configuration, |
| PietEventLogger pietEventLogger) { |
| return adapter; |
| } |
| |
| @Override |
| StreamScrollMonitor createStreamScrollMonitor( |
| ContentChangedListener contentChangedListener, MainThreadRunner mainThreadRunner) { |
| return streamScrollMonitor; |
| } |
| |
| @Override |
| LinearLayoutManager createRecyclerViewLayoutManager(Context context) { |
| return layoutManager; |
| } |
| |
| @Override |
| ScrollRestorer createScrollRestorer( |
| Configuration configuration, |
| RecyclerView recyclerView, |
| StreamScrollMonitor streamScrollMonitor, |
| /*@Nullable*/ ScrollState scrollState) { |
| return scrollRestorer; |
| } |
| |
| @Override |
| ScrollRestorer createNonRestoringScrollRestorer( |
| Configuration configuration, |
| RecyclerView recyclerView, |
| StreamScrollMonitor streamScrollMonitor) { |
| return nonRestoringScrollRestorer; |
| } |
| |
| @Override |
| ContextMenuManagerImpl createContextMenuManager( |
| RecyclerView recyclerView, MenuMeasurer menuMeasurer) { |
| return contextMenuManager; |
| } |
| |
| @Override |
| ViewLoggingUpdater createViewLoggingUpdater() { |
| return viewLoggingUpdater; |
| } |
| } |
| |
| private class LinearLayoutManagerWithFakePositioning extends LinearLayoutManager { |
| |
| private final List<View> childMap; |
| private int firstVisiblePosition = RecyclerView.NO_POSITION; |
| private int lastVisiblePosition = RecyclerView.NO_POSITION; |
| |
| public LinearLayoutManagerWithFakePositioning(Context context) { |
| super(context); |
| childMap = new ArrayList<>(); |
| } |
| |
| @Override |
| public int findFirstVisibleItemPosition() { |
| return firstVisiblePosition; |
| } |
| |
| @Override |
| public int findLastVisibleItemPosition() { |
| return lastVisiblePosition; |
| } |
| |
| @Override |
| public View findViewByPosition(int i) { |
| if (i < 0 || i >= childMap.size()) { |
| return null; |
| } |
| return childMap.get(i); |
| } |
| |
| private void addChildToPosition(int position, View child) { |
| childMap.add(position, child); |
| } |
| } |
| |
| private static class FakeKnownContentApi implements KnownContentApi { |
| |
| private final Set<KnownContentListener> listeners = new HashSet<>(); |
| |
| @Override |
| public void getKnownContent(Consumer<List<ContentMetadata>> knownContentConsumer) {} |
| |
| @Override |
| public void addListener(KnownContentListener listener) { |
| listeners.add(listener); |
| } |
| |
| @Override |
| public void removeListener(KnownContentListener listener) { |
| listeners.remove(listener); |
| } |
| |
| @Override |
| public KnownContentListener getKnownContentHostNotifier() { |
| return null; |
| } |
| } |
| } |