`showEmptyState()` -> EmptyStateUiHelper
As part of ongoing work to pull low-level empty state responsibilities
out of the pager adapter, this CL generally replicates the change
prototyped in ag/24516421/5..7, in smaller incremental steps to "show
the work." As originally described in that CL:
"... moves most of the low-level logic of `showEmptyState()` into
the UI helper. (...) also has the UI helper take responsibility for
setting the visibility of the main "list view" in sync (opposite of)
the empty state visibility."
As presented in this CL, the "incremental steps" per-snapshot are:
1. Extract most of the implementation directly to the new method at
`EmptyStateUiHelper.showEmptyState()`. The general functionality
is covered by existing integration tests (e.g., commenting-out the
new method body causes `UnbundledChooserActivityWorkProfileTest`
to fail). New `EmptyStateUiHelper` unit tests cover finer points
of the empty-state "button" conditions, and I've added a TODO
comment at one place legacy behavior seemingly may not align with
the original developer intent.
2. Also make the UI helper responsible for propagating empty-state
visibility changes back to the main list view (hiding the main
list when we show an empty state, and restoring it when the empty
state is hidden).
3. Look up all the sub-views during `EmptyStateUiHelper` construction
so we don't have to keep repeating their View IDs throughout.
4. Tighten visibility on `EmptyStateUiHelper.resetViewVisibilities()`
now that it's a private `showEmptyState()` implementation detail
(updated to package-protected/visible-for-testing). Also move the
method to the end of the class (after all the public methods).
Bug: 302311217
Test: IntentResolverUnitTests
Change-Id: Iac0cf3d62e2c3bf22afa6a2796ae4e731b706c02
diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java
index ad9614b..391cce7 100644
--- a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java
@@ -21,8 +21,6 @@
import android.os.UserHandle;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.TextView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
@@ -414,6 +412,8 @@
* The intention is to prevent the user from having to turn
* the work profile on if there will not be any apps resolved
* anyway.
+ *
+ * TODO: move this comment to the place where we configure our composite provider.
*/
public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) {
final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
@@ -449,48 +449,13 @@
}
}
- protected void showEmptyState(
+ private void showEmptyState(
ListAdapterT activeListAdapter,
EmptyState emptyState,
View.OnClickListener buttonOnClick) {
ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
- descriptor.mRootView.findViewById(
- com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
- descriptor.mEmptyStateUi.resetViewVisibilities();
- descriptor.setupContainerPadding();
-
- ViewGroup emptyStateView = descriptor.getEmptyStateView();
-
-
- TextView titleView = emptyStateView.findViewById(
- com.android.internal.R.id.resolver_empty_state_title);
- String title = emptyState.getTitle();
- if (title != null) {
- titleView.setVisibility(View.VISIBLE);
- titleView.setText(title);
- } else {
- titleView.setVisibility(View.GONE);
- }
-
- TextView subtitleView = emptyStateView.findViewById(
- com.android.internal.R.id.resolver_empty_state_subtitle);
- String subtitle = emptyState.getSubtitle();
- if (subtitle != null) {
- subtitleView.setVisibility(View.VISIBLE);
- subtitleView.setText(subtitle);
- } else {
- subtitleView.setVisibility(View.GONE);
- }
-
- View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty);
- defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
-
- Button button = emptyStateView.findViewById(
- com.android.internal.R.id.resolver_empty_state_button);
- button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
- button.setOnClickListener(buttonOnClick);
-
+ descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick);
activeListAdapter.markTabLoaded();
}
@@ -505,8 +470,6 @@
public void showListView(ListAdapterT activeListAdapter) {
ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
- descriptor.mRootView.findViewById(
- com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
descriptor.mEmptyStateUi.hide();
}
@@ -538,8 +501,10 @@
mAdapter = adapter;
mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list);
- mEmptyStateUi =
- new EmptyStateUiHelper(rootView, containerBottomPaddingOverrideSupplier);
+ mEmptyStateUi = new EmptyStateUiHelper(
+ rootView,
+ com.android.internal.R.id.resolver_list,
+ containerBottomPaddingOverrideSupplier);
}
protected ViewGroup getEmptyStateView() {
diff --git a/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java
index fc852f5..2f1e1b5 100644
--- a/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java
+++ b/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java
@@ -17,6 +17,11 @@
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.Optional;
import java.util.function.Supplier;
@@ -26,58 +31,111 @@
* some empty-state status.
*/
public class EmptyStateUiHelper {
- private final View mEmptyStateView;
private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
+ private final View mEmptyStateView;
+ private final View mListView;
+ private final View mEmptyStateContainerView;
+ private final TextView mEmptyStateTitleView;
+ private final TextView mEmptyStateSubtitleView;
+ private final Button mEmptyStateButtonView;
+ private final View mEmptyStateProgressView;
+ private final View mEmptyStateEmptyView;
public EmptyStateUiHelper(
ViewGroup rootView,
+ int listViewResourceId,
Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
mEmptyStateView =
rootView.requireViewById(com.android.internal.R.id.resolver_empty_state);
- mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
+ mListView = rootView.requireViewById(listViewResourceId);
+ mEmptyStateContainerView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_container);
+ mEmptyStateTitleView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_title);
+ mEmptyStateSubtitleView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_subtitle);
+ mEmptyStateButtonView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_button);
+ mEmptyStateProgressView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_progress);
+ mEmptyStateEmptyView = mEmptyStateView.requireViewById(com.android.internal.R.id.empty);
+ }
+
+ /**
+ * Display the described empty state.
+ * @param emptyState the data describing the cause of this empty-state condition.
+ * @param buttonOnClick handler for a button that the user might be able to use to circumvent
+ * the empty-state condition. If null, no button will be displayed.
+ */
+ public void showEmptyState(EmptyState emptyState, View.OnClickListener buttonOnClick) {
+ resetViewVisibilities();
+ setupContainerPadding();
+
+ String title = emptyState.getTitle();
+ if (title != null) {
+ mEmptyStateTitleView.setVisibility(View.VISIBLE);
+ mEmptyStateTitleView.setText(title);
+ } else {
+ mEmptyStateTitleView.setVisibility(View.GONE);
+ }
+
+ String subtitle = emptyState.getSubtitle();
+ if (subtitle != null) {
+ mEmptyStateSubtitleView.setVisibility(View.VISIBLE);
+ mEmptyStateSubtitleView.setText(subtitle);
+ } else {
+ mEmptyStateSubtitleView.setVisibility(View.GONE);
+ }
+
+ mEmptyStateEmptyView.setVisibility(
+ emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
+ // TODO: The EmptyState API says that if `useDefaultEmptyView()` is true, we'll ignore the
+ // state's specified title/subtitle; where (if anywhere) is that implemented?
+
+ mEmptyStateButtonView.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
+ mEmptyStateButtonView.setOnClickListener(buttonOnClick);
+
+ // Don't show the main list view when we're showing an empty state.
+ mListView.setVisibility(View.GONE);
}
/** Sets up the padding of the view containing the empty state screens. */
public void setupContainerPadding() {
- View container = mEmptyStateView.requireViewById(
- com.android.internal.R.id.resolver_empty_state_container);
Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
bottomPaddingOverride.ifPresent(paddingBottom ->
- container.setPadding(
- container.getPaddingLeft(),
- container.getPaddingTop(),
- container.getPaddingRight(),
+ mEmptyStateContainerView.setPadding(
+ mEmptyStateContainerView.getPaddingLeft(),
+ mEmptyStateContainerView.getPaddingTop(),
+ mEmptyStateContainerView.getPaddingRight(),
paddingBottom));
}
- public void resetViewVisibilities() {
- mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
- .setVisibility(View.VISIBLE);
- mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_subtitle)
- .setVisibility(View.VISIBLE);
- mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_button)
- .setVisibility(View.INVISIBLE);
- mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_progress)
- .setVisibility(View.GONE);
- mEmptyStateView.requireViewById(com.android.internal.R.id.empty)
- .setVisibility(View.GONE);
- mEmptyStateView.setVisibility(View.VISIBLE);
- }
-
public void showSpinner() {
- mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
- .setVisibility(View.INVISIBLE);
+ mEmptyStateTitleView.setVisibility(View.INVISIBLE);
// TODO: subtitle?
- mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_button)
- .setVisibility(View.INVISIBLE);
- mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_progress)
- .setVisibility(View.VISIBLE);
- mEmptyStateView.requireViewById(com.android.internal.R.id.empty)
- .setVisibility(View.GONE);
+ mEmptyStateButtonView.setVisibility(View.INVISIBLE);
+ mEmptyStateProgressView.setVisibility(View.VISIBLE);
+ mEmptyStateEmptyView.setVisibility(View.GONE);
}
public void hide() {
mEmptyStateView.setVisibility(View.GONE);
+ mListView.setVisibility(View.VISIBLE);
+ }
+
+ // TODO: this is exposed for testing so we can thoroughly prepare initial conditions that let us
+ // observe the resulting change. In reality it's only invoked as part of `showEmptyState()` and
+ // we could consider setting up narrower "realistic" preconditions to make assertions about the
+ // higher-level operation.
+ @VisibleForTesting
+ void resetViewVisibilities() {
+ mEmptyStateTitleView.setVisibility(View.VISIBLE);
+ mEmptyStateSubtitleView.setVisibility(View.VISIBLE);
+ mEmptyStateButtonView.setVisibility(View.INVISIBLE);
+ mEmptyStateProgressView.setVisibility(View.GONE);
+ mEmptyStateEmptyView.setVisibility(View.GONE);
+ mEmptyStateView.setVisibility(View.VISIBLE);
}
}
diff --git a/java/tests/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt b/java/tests/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt
index 27ed7e3..696dd1f 100644
--- a/java/tests/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt
+++ b/java/tests/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt
@@ -20,12 +20,18 @@
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
+import android.widget.TextView
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.any
+import com.android.intentresolver.emptystate.EmptyState
+import com.android.intentresolver.mock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import java.util.function.Supplier
import org.junit.Before
import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
class EmptyStateUiHelperTest {
private val context = InstrumentationRegistry.getInstrumentation().getContext()
@@ -37,8 +43,9 @@
}
lateinit var rootContainer: ViewGroup
- lateinit var emptyStateTitleView: View
- lateinit var emptyStateSubtitleView: View
+ lateinit var mainListView: View // Visible when no empty state is showing.
+ lateinit var emptyStateTitleView: TextView
+ lateinit var emptyStateSubtitleView: TextView
lateinit var emptyStateButtonView: View
lateinit var emptyStateProgressView: View
lateinit var emptyStateDefaultTextView: View
@@ -55,6 +62,7 @@
rootContainer,
true
)
+ mainListView = rootContainer.requireViewById(com.android.internal.R.id.resolver_list)
emptyStateRootView =
rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state)
emptyStateTitleView =
@@ -68,7 +76,12 @@
emptyStateDefaultTextView = rootContainer.requireViewById(com.android.internal.R.id.empty)
emptyStateContainerView =
rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_container)
- emptyStateUiHelper = EmptyStateUiHelper(rootContainer, containerPaddingSupplier)
+ emptyStateUiHelper =
+ EmptyStateUiHelper(
+ rootContainer,
+ com.android.internal.R.id.resolver_list,
+ containerPaddingSupplier
+ )
}
@Test
@@ -112,10 +125,12 @@
@Test
fun testHide() {
emptyStateRootView.visibility = View.VISIBLE
+ mainListView.visibility = View.GONE
emptyStateUiHelper.hide()
assertThat(emptyStateRootView.visibility).isEqualTo(View.GONE)
+ assertThat(mainListView.visibility).isEqualTo(View.VISIBLE)
}
@Test
@@ -143,4 +158,71 @@
assertThat(emptyStateContainerView.paddingRight).isEqualTo(3)
assertThat(emptyStateContainerView.paddingBottom).isEqualTo(42)
}
+
+ @Test
+ fun testShowEmptyState_noOnClickHandler() {
+ mainListView.visibility = View.VISIBLE
+
+ // Note: an `EmptyState.ClickListener` isn't invoked directly by the UI helper; it has to be
+ // built into the "on-click handler" that's injected to implement the button-press. We won't
+ // display the button without a click "handler," even if it *does* have a `ClickListener`.
+ val clickListener = mock<EmptyState.ClickListener>()
+
+ val emptyState =
+ object : EmptyState {
+ override fun getTitle() = "Test title"
+ override fun getSubtitle() = "Test subtitle"
+
+ override fun getButtonClickListener() = clickListener
+ }
+ emptyStateUiHelper.showEmptyState(emptyState, null)
+
+ assertThat(mainListView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateRootView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateTitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateSubtitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateButtonView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateProgressView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+
+ assertThat(emptyStateTitleView.text).isEqualTo("Test title")
+ assertThat(emptyStateSubtitleView.text).isEqualTo("Test subtitle")
+
+ verify(clickListener, never()).onClick(any())
+ }
+
+ @Test
+ fun testShowEmptyState_withOnClickHandlerAndClickListener() {
+ mainListView.visibility = View.VISIBLE
+
+ val clickListener = mock<EmptyState.ClickListener>()
+ val onClickHandler = mock<View.OnClickListener>()
+
+ val emptyState =
+ object : EmptyState {
+ override fun getTitle() = "Test title"
+ override fun getSubtitle() = "Test subtitle"
+
+ override fun getButtonClickListener() = clickListener
+ }
+ emptyStateUiHelper.showEmptyState(emptyState, onClickHandler)
+
+ assertThat(mainListView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateRootView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateTitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateSubtitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateButtonView.visibility).isEqualTo(View.VISIBLE) // Now shown.
+ assertThat(emptyStateProgressView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+
+ assertThat(emptyStateTitleView.text).isEqualTo("Test title")
+ assertThat(emptyStateSubtitleView.text).isEqualTo("Test subtitle")
+
+ emptyStateButtonView.performClick()
+
+ verify(onClickHandler).onClick(emptyStateButtonView)
+ // The test didn't explicitly configure its `OnClickListener` to relay the click event on
+ // to the `EmptyState.ClickListener`, so it still won't have fired here.
+ verify(clickListener, never()).onClick(any())
+ }
}