Introduce EmptyStateUiHelper (& initial tests)

Based on prototype work in ag/24516421, this CL introduces a new
component to handle the empty-state UI implementation details that had
previously been implemented in `MultiProfilePagerAdapter`, since those
details significantly clutter the implementation of that adapter's
other responsibilities.

As in ag/24516421 patchset #4, this just sets up the boilerplate and
kicks off with some "low-hanging-fruit" operations. Follow-up CLs will
continue migrating these responsibilities as in ag/24516421 (except
with more incremental testing).

Bug: 302311217
Test: IntentResolverUnitTests, CtsSharesheetDeviceTest
Change-Id: Ie9bb7f4e97836321521c3cf13c77cafc97b1a461
diff --git a/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
index 2c98d89..8c640dd 100644
--- a/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
@@ -29,6 +29,7 @@
 
 import com.android.intentresolver.emptystate.EmptyState;
 import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.EmptyStateUiHelper;
 import com.android.internal.annotations.VisibleForTesting;
 
 import com.google.common.collect.ImmutableList;
@@ -424,7 +425,7 @@
             clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
                 ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
                         userHandleToPageIndex(listAdapter.getUserHandle()));
-                MultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView());
+                descriptor.mEmptyStateUi.showSpinner();
             });
         }
 
@@ -451,9 +452,9 @@
                 userHandleToPageIndex(activeListAdapter.getUserHandle()));
         descriptor.mRootView.findViewById(
                 com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
+        descriptor.mEmptyStateUi.resetViewVisibilities();
+
         ViewGroup emptyStateView = descriptor.getEmptyStateView();
-        resetViewVisibilitiesForEmptyState(emptyStateView);
-        emptyStateView.setVisibility(View.VISIBLE);
 
         View container = emptyStateView.findViewById(
                 com.android.internal.R.id.resolver_empty_state_container);
@@ -504,36 +505,12 @@
                     paddingBottom));
     }
 
-    private void showSpinner(View emptyStateView) {
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title)
-                .setVisibility(View.INVISIBLE);
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button)
-                .setVisibility(View.INVISIBLE);
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress)
-                .setVisibility(View.VISIBLE);
-        emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
-    }
-
-    private void resetViewVisibilitiesForEmptyState(View emptyStateView) {
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title)
-                .setVisibility(View.VISIBLE);
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle)
-                .setVisibility(View.VISIBLE);
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button)
-                .setVisibility(View.INVISIBLE);
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress)
-                .setVisibility(View.GONE);
-        emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
-    }
-
     protected 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);
-        View emptyStateView = descriptor.mRootView.findViewById(
-                com.android.internal.R.id.resolver_empty_state);
-        emptyStateView.setVisibility(View.GONE);
+        descriptor.mEmptyStateUi.hide();
     }
 
     public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) {
@@ -547,6 +524,10 @@
     // should be the owner of all per-profile data (especially now that the API is generic)?
     private static class ProfileDescriptor<PageViewT, SinglePageAdapterT> {
         final ViewGroup mRootView;
+        final EmptyStateUiHelper mEmptyStateUi;
+
+        // TODO: post-refactoring, we may not need to retain these ivars directly (since they may
+        // be encapsulated within the `EmptyStateUiHelper`?).
         private final ViewGroup mEmptyStateView;
 
         private final SinglePageAdapterT mAdapter;
@@ -557,6 +538,7 @@
             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);
         }
 
         protected ViewGroup getEmptyStateView() {
diff --git a/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
new file mode 100644
index 0000000..d7ef8c7
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.android.intentresolver.emptystate;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Helper for building `MultiProfilePagerAdapter` tab UIs for profile tabs that are "blocked" by
+ * some empty-state status.
+ */
+public class EmptyStateUiHelper {
+    private final View mEmptyStateView;
+
+    public EmptyStateUiHelper(ViewGroup rootView) {
+        mEmptyStateView =
+                rootView.requireViewById(com.android.internal.R.id.resolver_empty_state);
+    }
+
+    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);
+        // 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);
+    }
+
+    public void hide() {
+        mEmptyStateView.setVisibility(View.GONE);
+    }
+}
+
diff --git a/java/tests/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt b/java/tests/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt
index 56034f0..ed06f7d 100644
--- a/java/tests/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt
+++ b/java/tests/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt
@@ -17,7 +17,9 @@
 package com.android.intentresolver
 
 import android.os.UserHandle
+import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewGroup
 import android.widget.ListView
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.intentresolver.MultiProfilePagerAdapter.PROFILE_PERSONAL
@@ -26,6 +28,7 @@
 import com.google.common.collect.ImmutableList
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
+import java.util.function.Supplier
 import org.junit.Test
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
@@ -35,6 +38,10 @@
     private val WORK_USER_HANDLE = UserHandle.of(20)
 
     private val context = InstrumentationRegistry.getInstrumentation().getContext()
+    private val inflater = Supplier {
+        LayoutInflater.from(context).inflate(R.layout.resolver_list_per_profile, null, false)
+            as ViewGroup
+    }
 
     @Test
     fun testSinglePageProfileAdapter() {
@@ -52,7 +59,7 @@
                 PROFILE_PERSONAL,
                 null,
                 null,
-                { ListView(context) },
+                inflater,
                 { Optional.empty() }
             )
         assertThat(pagerAdapter.count).isEqualTo(1)
@@ -86,7 +93,7 @@
                 PROFILE_PERSONAL,
                 WORK_USER_HANDLE, // TODO: why does this test pass even if this is null?
                 null,
-                { ListView(context) },
+                inflater,
                 { Optional.empty() }
             )
         assertThat(pagerAdapter.count).isEqualTo(2)
@@ -125,7 +132,7 @@
                 PROFILE_WORK, // <-- This test specifically requests we start on work profile.
                 WORK_USER_HANDLE, // TODO: why does this test pass even if this is null?
                 null,
-                { ListView(context) },
+                inflater,
                 { Optional.empty() }
             )
         assertThat(pagerAdapter.count).isEqualTo(2)
@@ -165,7 +172,7 @@
                 PROFILE_PERSONAL,
                 null,
                 null,
-                { ListView(context) },
+                inflater,
                 { Optional.empty() }
             )
         pagerAdapter.setupContainerPadding(container)
@@ -193,7 +200,7 @@
                 PROFILE_PERSONAL,
                 null,
                 null,
-                { ListView(context) },
+                inflater,
                 { Optional.of(42) }
             )
         pagerAdapter.setupContainerPadding(container)
@@ -227,7 +234,7 @@
                 PROFILE_WORK,
                 WORK_USER_HANDLE,
                 null,
-                { ListView(context) },
+                inflater,
                 { Optional.empty() }
             )
         assertThat(pagerAdapter.shouldShowEmptyStateScreen(workListAdapter)).isTrue()
@@ -261,7 +268,7 @@
                 PROFILE_WORK,
                 WORK_USER_HANDLE,
                 null,
-                { ListView(context) },
+                inflater,
                 { Optional.empty() }
             )
         assertThat(pagerAdapter.shouldShowEmptyStateScreen(workListAdapter)).isFalse()
diff --git a/java/tests/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt b/java/tests/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt
new file mode 100644
index 0000000..bc5545d
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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.android.intentresolver.emptystate
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+class EmptyStateUiHelperTest {
+    private val context = InstrumentationRegistry.getInstrumentation().getContext()
+
+    lateinit var rootContainer: ViewGroup
+    lateinit var emptyStateTitleView: View
+    lateinit var emptyStateSubtitleView: View
+    lateinit var emptyStateButtonView: View
+    lateinit var emptyStateProgressView: View
+    lateinit var emptyStateDefaultTextView: View
+    lateinit var emptyStateContainerView: View
+    lateinit var emptyStateRootView: View
+    lateinit var emptyStateUiHelper: EmptyStateUiHelper
+
+    @Before
+    fun setup() {
+        rootContainer = FrameLayout(context)
+        LayoutInflater.from(context)
+            .inflate(
+                com.android.intentresolver.R.layout.resolver_list_per_profile,
+                rootContainer,
+                true
+            )
+        emptyStateRootView =
+            rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state)
+        emptyStateTitleView =
+            rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
+        emptyStateSubtitleView = rootContainer.requireViewById(
+            com.android.internal.R.id.resolver_empty_state_subtitle)
+        emptyStateButtonView = rootContainer.requireViewById(
+            com.android.internal.R.id.resolver_empty_state_button)
+        emptyStateProgressView = rootContainer.requireViewById(
+            com.android.internal.R.id.resolver_empty_state_progress)
+        emptyStateDefaultTextView =
+            rootContainer.requireViewById(com.android.internal.R.id.empty)
+        emptyStateContainerView = rootContainer.requireViewById(
+            com.android.internal.R.id.resolver_empty_state_container)
+        emptyStateUiHelper = EmptyStateUiHelper(rootContainer)
+    }
+
+    @Test
+    fun testResetViewVisibilities() {
+        // First set each view's visibility to differ from the expected "reset" state so we can then
+        // assert that they're all reset afterward.
+        // TODO: for historic reasons "reset" doesn't cover `emptyStateContainerView`; should it?
+        emptyStateRootView.visibility = View.GONE
+        emptyStateTitleView.visibility = View.GONE
+        emptyStateSubtitleView.visibility = View.GONE
+        emptyStateButtonView.visibility = View.VISIBLE
+        emptyStateProgressView.visibility = View.VISIBLE
+        emptyStateDefaultTextView.visibility = View.VISIBLE
+
+        emptyStateUiHelper.resetViewVisibilities()
+
+        assertThat(emptyStateRootView.visibility).isEqualTo(View.VISIBLE)
+        assertThat(emptyStateTitleView.visibility).isEqualTo(View.VISIBLE)
+        assertThat(emptyStateSubtitleView.visibility).isEqualTo(View.VISIBLE)
+        assertThat(emptyStateButtonView.visibility).isEqualTo(View.INVISIBLE)
+        assertThat(emptyStateProgressView.visibility).isEqualTo(View.GONE)
+        assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun testShowSpinner() {
+        emptyStateTitleView.visibility = View.VISIBLE
+        emptyStateButtonView.visibility = View.VISIBLE
+        emptyStateProgressView.visibility = View.GONE
+        emptyStateDefaultTextView.visibility = View.VISIBLE
+
+        emptyStateUiHelper.showSpinner()
+
+        // TODO: should this cover any other views? Subtitle?
+        assertThat(emptyStateTitleView.visibility).isEqualTo(View.INVISIBLE)
+        assertThat(emptyStateButtonView.visibility).isEqualTo(View.INVISIBLE)
+        assertThat(emptyStateProgressView.visibility).isEqualTo(View.VISIBLE)
+        assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun testHide() {
+        emptyStateRootView.visibility = View.VISIBLE
+
+        emptyStateUiHelper.hide()
+
+        assertThat(emptyStateRootView.visibility).isEqualTo(View.GONE)
+    }
+}