Commit 92bfb8c2 authored by Wenyu Fu's avatar Wenyu Fu Committed by Commit Bot

[CCTToSFRE] Add a11y support for loading spinner

Add the content description for the loading spinner so that the screen
reader can read out the loading status correctly.

Change-Id: Ifa46b4b1c2d8255a592751132d56f66edb8b1257
Bug: 1119587
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2368445
Commit-Queue: Wenyu Fu <wenyufu@chromium.org>
Reviewed-by: default avatarSky Malice <skym@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#805951}
parent bbc08d6a
......@@ -46,13 +46,26 @@
android:text="@string/fre_welcome"
style="@style/FreTitle" />
<org.chromium.components.browser_ui.widget.LoadingView
android:id="@+id/progress_spinner_large"
style="@style/Widget.AppCompat.ProgressBar"
<!-- The FrameLayout here is to facilitate adding a proper content description for
the loading view. During development, it didn't seem possible to override the
LoadingView contentDescription in XML, but if there's support for this at some
point then we can remove the FrameLayout. -->
<FrameLayout
android:id="@+id/loading_view_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_height="@dimen/fre_loading_spinner_size"
android:layout_width="@dimen/fre_loading_spinner_size"
android:visibility="gone"/>
android:visibility="gone"
android:contentDescription="@string/sync_loading">
<org.chromium.components.browser_ui.widget.LoadingView
android:id="@+id/progress_spinner_large"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_height="@dimen/fre_loading_spinner_size"
android:layout_width="@dimen/fre_loading_spinner_size"
android:visibility="gone"/>
</FrameLayout>
<LinearLayout
android:id="@+id/fre_content_wrapper"
......
......@@ -201,6 +201,10 @@ public class ToSAndUMAFirstRunFragment extends Fragment implements FirstRunFragm
}
}
protected View getToSAndPrivacyText() {
return mTosAndPrivacy;
}
/**
* @return Whether the check box for Uma metrics can be shown. It should be used in conjunction
* with whether other non-spinner elements can generally be shown.
......
......@@ -8,6 +8,7 @@ import android.content.Context;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
......@@ -46,6 +47,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupport
}
private boolean mViewCreated;
private View mLoadingSpinnerContainer;
private LoadingView mLoadingSpinner;
private CallbackController mCallbackController;
private PolicyService.Observer mPolicyServiceObserver;
......@@ -109,6 +111,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupport
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mLoadingSpinnerContainer = view.findViewById(R.id.loading_view_container);
mLoadingSpinner = view.findViewById(R.id.progress_spinner_large);
mViewCreated = true;
mViewCreatedTimeMs = SystemClock.elapsedRealtime();
......@@ -137,6 +140,11 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupport
return super.canShowUmaCheckBox() && confirmedToShowUmaAndTos();
}
@Override
public void onShowLoadingUIComplete() {
mLoadingSpinnerContainer.setVisibility(View.VISIBLE);
}
@Override
public void onHideLoadingUIComplete() {
RecordHistogram.recordTimesHistogram("MobileFre.CctTos.LoadingDuration",
......@@ -147,7 +155,14 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupport
} else {
// Else, show the UMA as the loading spinner is GONE.
assert confirmedToShowUmaAndTos();
boolean hasAccessibilityFocus = mLoadingSpinnerContainer.isAccessibilityFocused();
mLoadingSpinnerContainer.setVisibility(View.GONE);
setTosAndUmaVisible(true);
if (hasAccessibilityFocus) {
getToSAndPrivacyText().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
}
}
......
......@@ -36,7 +36,7 @@ public class TosAndUmaFragmentView extends FrameLayout {
private View mTitle;
private View mLogo;
private View mLoadingSpinner;
private View mLoadingSpinnerContainer;
private View mShadow;
private int mLastHeight;
......@@ -72,7 +72,7 @@ public class TosAndUmaFragmentView extends FrameLayout {
mTitle = findViewById(R.id.title);
mLogo = findViewById(R.id.image);
mLoadingSpinner = findViewById(R.id.progress_spinner_large);
mLoadingSpinnerContainer = findViewById(R.id.loading_view_container);
mShadow = findViewById(R.id.shadow);
// Set up shadow.
......@@ -146,7 +146,7 @@ public class TosAndUmaFragmentView extends FrameLayout {
private void setSpinnerLayoutParams(boolean useWideScreen, int width, int height) {
LinearLayout.LayoutParams spinnerParams =
(LinearLayout.LayoutParams) mLoadingSpinner.getLayoutParams();
(LinearLayout.LayoutParams) mLoadingSpinnerContainer.getLayoutParams();
// Adjust the spinner placement. If in portrait mode, the spinner is centered in the region
// below the title; If in wide screen mode, the spinner is placed in the center of
......@@ -175,7 +175,7 @@ public class TosAndUmaFragmentView extends FrameLayout {
spinnerParams.topMargin = spinnerTopMargin;
}
mLoadingSpinner.setLayoutParams(spinnerParams);
mLoadingSpinnerContainer.setLayoutParams(spinnerParams);
}
private void setLogoLayoutParams(boolean useWideScreen, int height) {
......
......@@ -144,7 +144,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
launchFirstRunThroughCustomTab();
assertUIState(FragmentState.LOADING);
setAppRestrictiosnMockInitialized(false);
setAppRestrictionsMockInitialized(false);
assertUIState(FragmentState.NO_POLICY);
Assert.assertEquals(1,
......@@ -157,7 +157,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
// TODO(crbug.com/1120859): Test the policy check when native initializes before inflation.
// This will be possible when FragmentScenario is available.
public void testDialogEnabled() {
setAppRestrictiosnMockInitialized(true);
setAppRestrictionsMockInitialized(true);
launchFirstRunThroughCustomTab();
assertUIState(FragmentState.LOADING);
......@@ -174,7 +174,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
@Test
@SmallTest
public void testNotOwnedDevice() {
setAppRestrictiosnMockInitialized(true);
setAppRestrictionsMockInitialized(true);
launchFirstRunThroughCustomTab();
assertUIState(FragmentState.LOADING);
......@@ -191,7 +191,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
@Test
@SmallTest
public void testNotOwnedDevice_beforeInflation() {
setAppRestrictiosnMockInitialized(true);
setAppRestrictionsMockInitialized(true);
setEnterpriseInfoInitializedWithDeviceOwner(false);
launchFirstRunThroughCustomTab();
......@@ -208,7 +208,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
@Test
@SmallTest
public void testSkip_DeviceOwnedThenDialogPolicy() {
setAppRestrictiosnMockInitialized(true);
setAppRestrictionsMockInitialized(true);
launchFirstRunThroughCustomTab();
assertUIState(FragmentState.LOADING);
......@@ -232,7 +232,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
@Test
@SmallTest
public void testSkip_DialogPolicyThenDeviceOwned() {
setAppRestrictiosnMockInitialized(true);
setAppRestrictionsMockInitialized(true);
launchFirstRunThroughCustomTab();
assertUIState(FragmentState.LOADING);
......@@ -267,7 +267,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
assertUIState(FragmentState.HAS_POLICY);
// assertUIState will verify that exit was not called a second time.
setAppRestrictiosnMockInitialized(true);
setAppRestrictionsMockInitialized(true);
assertUIState(FragmentState.HAS_POLICY);
Assert.assertEquals(1,
......@@ -284,7 +284,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
@Test
@SmallTest
public void testNullOwnedState() {
setAppRestrictiosnMockInitialized(true);
setAppRestrictionsMockInitialized(true);
setPolicyServiceMockInitializedWithDialogEnabled(false);
launchFirstRunThroughCustomTab();
assertUIState(FragmentState.LOADING);
......@@ -367,7 +367,7 @@ public class TosAndUmaFirstRunFragmentWithEnterpriseSupportTest {
.getHasAppRestriction(any());
}
private void setAppRestrictiosnMockInitialized(boolean hasAppRestrictons) {
private void setAppRestrictionsMockInitialized(boolean hasAppRestrictons) {
Mockito.doAnswer(invocation -> {
Callback<Boolean> callback = invocation.getArgument(0);
callback.onResult(hasAppRestrictons);
......
......@@ -30,6 +30,12 @@ public class LoadingView extends ProgressBar {
* A observer interface that will be notified when the progress bar is hidden.
*/
public interface Observer {
/**
* Notify the listener a call to {@link #showLoadingUI()} is complete and loading view
* is VISIBLE.
*/
void onShowLoadingUIComplete();
/**
* Notify the listener a call to {@link #hideLoadingUI()} is complete and loading view is
* GONE.
......@@ -40,7 +46,7 @@ public class LoadingView extends ProgressBar {
private long mStartTime = -1;
private boolean mDisableAnimationForTest;
private final List<Observer> mListeners = new ArrayList<>();
private final List<Observer> mObservers = new ArrayList<>();
private final Runnable mDelayedShow = new Runnable() {
@Override
......@@ -49,6 +55,10 @@ public class LoadingView extends ProgressBar {
mStartTime = SystemClock.elapsedRealtime();
setVisibility(View.VISIBLE);
setAlpha(1.0f);
for (Observer observer : mObservers) {
observer.onShowLoadingUIComplete();
}
}
};
......@@ -132,7 +142,7 @@ public class LoadingView extends ProgressBar {
public void destroy() {
removeCallbacks(mDelayedShow);
removeCallbacks(mDelayedHide);
mListeners.clear();
mObservers.clear();
}
/**
......@@ -142,12 +152,12 @@ public class LoadingView extends ProgressBar {
* completely hidden with {@link #hideLoadingUI()}.
*/
public void addObserver(Observer listener) {
mListeners.add(listener);
mObservers.add(listener);
}
private void onHideLoadingFinished() {
setVisibility(GONE);
for (Observer observer : mListeners) {
for (Observer observer : mObservers) {
observer.onHideLoadingUIComplete();
}
}
......
......@@ -30,11 +30,25 @@ import java.util.concurrent.TimeUnit;
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE, shadows = {ShadowView.class})
public class LoadingViewTest {
static class TestObserver implements LoadingView.Observer {
public final CallbackHelper showLoadingCallback = new CallbackHelper();
public final CallbackHelper hideLoadingCallback = new CallbackHelper();
@Override
public void onShowLoadingUIComplete() {
showLoadingCallback.notifyCalled();
}
@Override
public void onHideLoadingUIComplete() {
hideLoadingCallback.notifyCalled();
}
}
private LoadingView mLoadingView;
private Activity mActivity;
private final CallbackHelper mCallback1 = new CallbackHelper();
private final CallbackHelper mCallback2 = new CallbackHelper();
private final TestObserver mTestObserver1 = new TestObserver();
private final TestObserver mTestObserver2 = new TestObserver();
@Before
public void setUpTest() throws Exception {
......@@ -47,26 +61,36 @@ public class LoadingViewTest {
mLoadingView.setDisableAnimationForTest(true);
content.addView(mLoadingView);
mLoadingView.addObserver(mCallback1::notifyCalled);
mLoadingView.addObserver(mCallback2::notifyCalled);
mLoadingView.addObserver(mTestObserver1);
mLoadingView.addObserver(mTestObserver2);
}
@Test
@SmallTest
public void testLoadingFast() {
mLoadingView.showLoadingUI();
Assert.assertEquals(
"showLoadingCallback1 should not be executed as soon as showLoadingUI is called.",
0, mTestObserver1.showLoadingCallback.getCallCount());
Assert.assertEquals(
"showLoadingCallback2 should not be executed as soon as showLoadingUI is called.",
0, mTestObserver2.showLoadingCallback.getCallCount());
ShadowLooper.idleMainLooper(100, TimeUnit.MILLISECONDS);
Assert.assertEquals("Progress bar should be hidden before 500ms.", View.GONE,
mLoadingView.getVisibility());
Assert.assertEquals("showLoadingCallback1 should not be executed with loading fast.", 0,
mTestObserver1.showLoadingCallback.getCallCount());
Assert.assertEquals("showLoadingCallback2 should not be executed with loading fast.", 0,
mTestObserver2.showLoadingCallback.getCallCount());
mLoadingView.hideLoadingUI();
Assert.assertEquals(
"Progress bar should never be visible.", View.GONE, mLoadingView.getVisibility());
Assert.assertEquals("Callback1 should be executed after loading finishes.", 1,
mCallback1.getCallCount());
Assert.assertEquals("Callback2 should be executed after loading finishes.", 1,
mCallback2.getCallCount());
Assert.assertEquals("hideLoadingCallback1 should be executed after loading finishes.", 1,
mTestObserver1.hideLoadingCallback.getCallCount());
Assert.assertEquals("hideLoadingCallback2 should be executed after loading finishes.", 1,
mTestObserver2.hideLoadingCallback.getCallCount());
}
@Test
......@@ -74,26 +98,36 @@ public class LoadingViewTest {
public void testLoadingSlow() {
long sleepTime = 500;
mLoadingView.showLoadingUI();
Assert.assertEquals(
"showLoadingCallback1 should not be executed as soon as showLoadingUI is called.",
0, mTestObserver1.showLoadingCallback.getCallCount());
Assert.assertEquals(
"showLoadingCallback2 should not be executed as soon as showLoadingUI is called.",
0, mTestObserver2.showLoadingCallback.getCallCount());
ShadowLooper.idleMainLooper(sleepTime, TimeUnit.MILLISECONDS);
Assert.assertEquals("Progress bar should be visible after 500ms.", View.VISIBLE,
mLoadingView.getVisibility());
Assert.assertEquals("showLoadingCallback1 should be executed when spinner is visible.", 1,
mTestObserver1.showLoadingCallback.getCallCount());
Assert.assertEquals("showLoadingCallback2 should be executed when spinner is visible.", 1,
mTestObserver2.showLoadingCallback.getCallCount());
mLoadingView.hideLoadingUI();
Assert.assertEquals("Progress bar should still be visible until showing for 500ms.",
View.VISIBLE, mLoadingView.getVisibility());
Assert.assertEquals("Callback1 should not be executed before loading finishes.", 0,
mCallback1.getCallCount());
Assert.assertEquals("Callback2 should not be executed before loading finishes.", 0,
mCallback2.getCallCount());
Assert.assertEquals("hideLoadingCallback1 should not be executed before loading finishes.",
0, mTestObserver1.hideLoadingCallback.getCallCount());
Assert.assertEquals("hideLoadingCallback2 should not be executed before loading finishes.",
0, mTestObserver2.hideLoadingCallback.getCallCount());
// The spinner should be displayed for at least 500ms.
ShadowLooper.idleMainLooper(sleepTime, TimeUnit.MILLISECONDS);
Assert.assertEquals("Progress bar should be hidden after 500ms.", View.GONE,
mLoadingView.getVisibility());
Assert.assertEquals("Callback1 should be executed after loading finishes.", 1,
mCallback1.getCallCount());
Assert.assertEquals("Callback2 should be executed after loading finishes.", 1,
mCallback2.getCallCount());
Assert.assertEquals("hideLoadingCallback1 should be executed after loading finishes.", 1,
mTestObserver1.hideLoadingCallback.getCallCount());
Assert.assertEquals("hideLoadingCallback2 should be executed after loading finishes.", 1,
mTestObserver2.hideLoadingCallback.getCallCount());
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment