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)
+ }
+}