Asynchronously inflate the bouncer.

As we are lazily inflating the bouncer every time we show it, we can get
huge performance gains from AsyncInflater. From the class documentation
"This is intended for parts of the UI that are created lazily or in
response to user interactions. This allows the UI thread to continue to
be responsive & animate while the relatively heavy inflate is being
performed." it seems that this is the perfect use case.

I've added the AsyncInflater in a dagger module for testing purposes. I
also moved the handler constructrion in KeyguardMessageArea to its
corresponding view controller to conform to the AsynInflater
requirements.

Interestingly this also fixes b/265384692 as it seemed like the bouncer
was being inflated before the onclicklisteners were added.

Fixes: 269522372
Fixes: 265384692
Test: Test for talk back to ensure that it announces the text view.
Test: open scrimmed and non scrimmed bouncer.
Test: Pin, Password, pattern, simpin, simpuk
Test: modified unit tests.

Change-Id: Ic65af22da9f6822e5137aae3cfee03a6e819ab94
This commit is contained in:
Aaron Liu
2023-03-06 11:48:40 -08:00
parent f7fa5cb414
commit e3c687ce3f
9 changed files with 178 additions and 68 deletions

View File

@@ -18,13 +18,9 @@ package com.android.keyguard;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -33,22 +29,10 @@ import androidx.annotation.Nullable;
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.R;
import java.lang.ref.WeakReference;
/***
* Manages a number of views inside of the given layout. See below for a list of widgets.
*/
public abstract class KeyguardMessageArea extends TextView implements SecurityMessageDisplay {
/** Handler token posted with accessibility announcement runnables. */
private static final Object ANNOUNCE_TOKEN = new Object();
/**
* Delay before speaking an accessibility announcement. Used to prevent
* lift-to-type from interrupting itself.
*/
private static final long ANNOUNCEMENT_DELAY = 250;
private final Handler mHandler;
private CharSequence mMessage;
private boolean mIsVisible;
@@ -65,7 +49,6 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe
super(context, attrs);
setLayerType(LAYER_TYPE_HARDWARE, null); // work around nested unclipped SaveLayer bug
mHandler = new Handler(Looper.myLooper());
onThemeChanged();
}
@@ -127,9 +110,6 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe
private void securityMessageChanged(CharSequence message) {
mMessage = message;
update();
mHandler.removeCallbacksAndMessages(ANNOUNCE_TOKEN);
mHandler.postAtTime(new AnnounceRunnable(this, getText()), ANNOUNCE_TOKEN,
(SystemClock.uptimeMillis() + ANNOUNCEMENT_DELAY));
}
private void clearMessage() {
@@ -156,25 +136,4 @@ public abstract class KeyguardMessageArea extends TextView implements SecurityMe
/** Set the text color */
protected abstract void updateTextColor();
/**
* Runnable used to delay accessibility announcements.
*/
private static class AnnounceRunnable implements Runnable {
private final WeakReference<View> mHost;
private final CharSequence mTextToAnnounce;
AnnounceRunnable(View host, CharSequence textToAnnounce) {
mHost = new WeakReference<View>(host);
mTextToAnnounce = textToAnnounce;
}
@Override
public void run() {
final View host = mHost.get();
if (host != null) {
host.announceForAccessibility(mTextToAnnounce);
}
}
}
}

View File

@@ -18,11 +18,17 @@ package com.android.keyguard;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.util.ViewController;
import java.lang.ref.WeakReference;
import javax.inject.Inject;
/**
@@ -31,8 +37,14 @@ import javax.inject.Inject;
*/
public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
extends ViewController<T> {
/**
* Delay before speaking an accessibility announcement. Used to prevent
* lift-to-type from interrupting itself.
*/
private static final long ANNOUNCEMENT_DELAY = 250;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private final ConfigurationController mConfigurationController;
private final AnnounceRunnable mAnnounceRunnable;
private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
public void onFinishedGoingToSleep(int why) {
@@ -68,6 +80,7 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mConfigurationController = configurationController;
mAnnounceRunnable = new AnnounceRunnable(mView);
}
@Override
@@ -100,6 +113,12 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
*/
public void setMessage(CharSequence s, boolean animate) {
mView.setMessage(s, animate);
CharSequence msg = mView.getText();
if (!TextUtils.isEmpty(msg)) {
mView.removeCallbacks(mAnnounceRunnable);
mAnnounceRunnable.setTextToAnnounce(msg);
mView.postDelayed(mAnnounceRunnable, ANNOUNCEMENT_DELAY);
}
}
public void setMessage(int resId) {
@@ -134,4 +153,30 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea>
view, mKeyguardUpdateMonitor, mConfigurationController);
}
}
/**
* Runnable used to delay accessibility announcements.
*/
@VisibleForTesting
public static class AnnounceRunnable implements Runnable {
private final WeakReference<View> mHost;
private CharSequence mTextToAnnounce;
AnnounceRunnable(View host) {
mHost = new WeakReference<>(host);
}
/** Sets the text to announce. */
public void setTextToAnnounce(CharSequence textToAnnounce) {
mTextToAnnounce = textToAnnounce;
}
@Override
public void run() {
final View host = mHost.get();
if (host != null) {
host.announceForAccessibility(mTextToAnnounce);
}
}
}
}

View File

@@ -1065,23 +1065,28 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
}
private void reloadColors() {
reinflateViewFlipper();
mView.reloadColors();
reinflateViewFlipper(() -> mView.reloadColors());
}
/** Handles density or font scale changes. */
private void onDensityOrFontScaleChanged() {
reinflateViewFlipper();
mView.onDensityOrFontScaleChanged();
reinflateViewFlipper(() -> mView.onDensityOrFontScaleChanged());
}
/**
* Reinflate the view flipper child view.
*/
public void reinflateViewFlipper() {
public void reinflateViewFlipper(
KeyguardSecurityViewFlipperController.OnViewInflatedListener onViewInflatedListener) {
mSecurityViewFlipperController.clearViews();
mSecurityViewFlipperController.getSecurityView(mCurrentSecurityMode,
mKeyguardSecurityCallback);
if (mFeatureFlags.isEnabled(Flags.ASYNC_INFLATE_BOUNCER)) {
mSecurityViewFlipperController.asynchronouslyInflateView(mCurrentSecurityMode,
mKeyguardSecurityCallback, onViewInflatedListener);
} else {
mSecurityViewFlipperController.getSecurityView(mCurrentSecurityMode,
mKeyguardSecurityCallback);
onViewInflatedListener.onViewInflated();
}
}
/**

View File

@@ -19,11 +19,16 @@ package com.android.keyguard;
import android.util.Log;
import android.view.LayoutInflater;
import androidx.annotation.Nullable;
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.KeyguardInputViewController.Factory;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.keyguard.dagger.KeyguardBouncerScope;
import com.android.systemui.R;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.util.ViewController;
import java.util.ArrayList;
@@ -44,18 +49,24 @@ public class KeyguardSecurityViewFlipperController
private final List<KeyguardInputViewController<KeyguardInputView>> mChildren =
new ArrayList<>();
private final LayoutInflater mLayoutInflater;
private final AsyncLayoutInflater mAsyncLayoutInflater;
private final EmergencyButtonController.Factory mEmergencyButtonControllerFactory;
private final Factory mKeyguardSecurityViewControllerFactory;
private final FeatureFlags mFeatureFlags;
@Inject
protected KeyguardSecurityViewFlipperController(KeyguardSecurityViewFlipper view,
LayoutInflater layoutInflater,
AsyncLayoutInflater asyncLayoutInflater,
KeyguardInputViewController.Factory keyguardSecurityViewControllerFactory,
EmergencyButtonController.Factory emergencyButtonControllerFactory) {
EmergencyButtonController.Factory emergencyButtonControllerFactory,
FeatureFlags featureFlags) {
super(view);
mKeyguardSecurityViewControllerFactory = keyguardSecurityViewControllerFactory;
mLayoutInflater = layoutInflater;
mEmergencyButtonControllerFactory = emergencyButtonControllerFactory;
mAsyncLayoutInflater = asyncLayoutInflater;
mFeatureFlags = featureFlags;
}
@Override
@@ -92,13 +103,12 @@ public class KeyguardSecurityViewFlipperController
}
}
if (childController == null
if (!mFeatureFlags.isEnabled(Flags.ASYNC_INFLATE_BOUNCER) && childController == null
&& securityMode != SecurityMode.None && securityMode != SecurityMode.Invalid) {
int layoutId = getLayoutIdFor(securityMode);
KeyguardInputView view = null;
if (layoutId != 0) {
if (DEBUG) Log.v(TAG, "inflating id = " + layoutId);
if (DEBUG) Log.v(TAG, "inflating on main thread id = " + layoutId);
view = (KeyguardInputView) mLayoutInflater.inflate(
layoutId, mView, false);
mView.addView(view);
@@ -119,6 +129,36 @@ public class KeyguardSecurityViewFlipperController
return childController;
}
/**
* Asynchronously inflate view and then add it to view flipper on the main thread when complete.
*
* OnInflateFinishedListener will be called on the main thread.
*
* @param securityMode
* @param keyguardSecurityCallback
*/
public void asynchronouslyInflateView(SecurityMode securityMode,
KeyguardSecurityCallback keyguardSecurityCallback,
@Nullable OnViewInflatedListener onViewInflatedListener) {
int layoutId = getLayoutIdFor(securityMode);
if (layoutId != 0) {
if (DEBUG) Log.v(TAG, "inflating on bg thread id = " + layoutId);
mAsyncLayoutInflater.inflate(layoutId, mView,
(view, resId, parent) -> {
mView.addView(view);
KeyguardInputViewController<KeyguardInputView> childController =
mKeyguardSecurityViewControllerFactory.create(
(KeyguardInputView) view, securityMode,
keyguardSecurityCallback);
childController.init();
mChildren.add(childController);
if (onViewInflatedListener != null) {
onViewInflatedListener.onViewInflated();
}
});
}
}
private int getLayoutIdFor(SecurityMode securityMode) {
switch (securityMode) {
case Pattern: return R.layout.keyguard_pattern_view;
@@ -162,4 +202,10 @@ public class KeyguardSecurityViewFlipperController
return 0;
}
}
/** Listener to when view has finished inflation. */
public interface OnViewInflatedListener {
/** Notifies that view has been inflated */
void onViewInflated();
}
}

View File

@@ -97,6 +97,7 @@ import android.view.accessibility.CaptioningManager;
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.TextClassificationManager;
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
import androidx.core.app.NotificationManagerCompat;
import com.android.internal.app.IBatteryStats;
@@ -388,6 +389,13 @@ public class FrameworkServicesModule {
return LayoutInflater.from(context);
}
/** */
@Provides
@Singleton
public AsyncLayoutInflater provideAsyncLayoutInflater(Context context) {
return new AsyncLayoutInflater(context);
}
@Provides
static MediaProjectionManager provideMediaProjectionManager(Context context) {
return context.getSystemService(MediaProjectionManager.class);

View File

@@ -34,7 +34,6 @@ import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransition
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.ActivityStarter
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
@@ -112,16 +111,18 @@ object KeyguardBouncerViewBinder {
launch {
viewModel.show.collect {
// Reset Security Container entirely.
view.visibility = View.VISIBLE
securityContainerController.onBouncerVisibilityChanged(
/* isVisible= */ true
)
securityContainerController.reinflateViewFlipper()
securityContainerController.showPrimarySecurityScreen(
/* turningOff= */ false
)
securityContainerController.appear()
securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
securityContainerController.reinflateViewFlipper {
// Reset Security Container entirely.
view.visibility = View.VISIBLE
securityContainerController.onBouncerVisibilityChanged(
/* isVisible= */ true
)
securityContainerController.showPrimarySecurityScreen(
/* turningOff= */ false
)
securityContainerController.appear()
securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON)
}
}
}

View File

@@ -18,12 +18,15 @@ package com.android.keyguard;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -37,6 +40,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@TestableLooper.RunWithLooper
@RunWith(AndroidTestingRunner.class)
public class KeyguardMessageAreaControllerTest extends SysuiTestCase {
@Mock
@@ -45,14 +49,14 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase {
private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Mock
private KeyguardMessageArea mKeyguardMessageArea;
private KeyguardMessageAreaController mMessageAreaController;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mMessageAreaController = new KeyguardMessageAreaController.Factory(
mKeyguardUpdateMonitor, mConfigurationController).create(mKeyguardMessageArea);
mKeyguardUpdateMonitor, mConfigurationController).create(
mKeyguardMessageArea);
}
@Test
@@ -88,6 +92,19 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase {
verify(mKeyguardMessageArea).setMessage("", /* animate= */ true);
}
@Test
public void testSetMessage_AnnounceForAccessibility() {
ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);
when(mKeyguardMessageArea.getText()).thenReturn("abc");
mMessageAreaController.setMessage("abc");
verify(mKeyguardMessageArea).setMessage("abc", /* animate= */ true);
verify(mKeyguardMessageArea).removeCallbacks(any(Runnable.class));
verify(mKeyguardMessageArea).postDelayed(argumentCaptor.capture(), anyLong());
argumentCaptor.getValue().run();
verify(mKeyguardMessageArea).announceForAccessibility("abc");
}
@Test
public void testSetBouncerVisible() {
mMessageAreaController.setIsVisible(true);

View File

@@ -66,6 +66,7 @@ import com.android.systemui.biometrics.SideFpsUiRequestSource;
import com.android.systemui.classifier.FalsingA11yDelegate;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
@@ -617,12 +618,25 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase {
@Test
public void testReinflateViewFlipper() {
mKeyguardSecurityContainerController.reinflateViewFlipper();
mKeyguardSecurityContainerController.reinflateViewFlipper(() -> {});
verify(mKeyguardSecurityViewFlipperController).clearViews();
verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
any(KeyguardSecurityCallback.class));
}
@Test
public void testReinflateViewFlipper_asyncBouncerFlagOn() {
when(mFeatureFlags.isEnabled(Flags.ASYNC_INFLATE_BOUNCER)).thenReturn(true);
KeyguardSecurityViewFlipperController.OnViewInflatedListener onViewInflatedListener =
() -> {
};
mKeyguardSecurityContainerController.reinflateViewFlipper(onViewInflatedListener);
verify(mKeyguardSecurityViewFlipperController).clearViews();
verify(mKeyguardSecurityViewFlipperController).asynchronouslyInflateView(
any(SecurityMode.class),
any(KeyguardSecurityCallback.class), eq(onViewInflatedListener));
}
@Test
public void testSideFpsControllerShow() {
mKeyguardSecurityContainerController.updateSideFpsVisibility(/* isVisible= */ true);

View File

@@ -31,10 +31,12 @@ import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.view.WindowInsetsController;
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FeatureFlags;
import org.junit.Before;
import org.junit.Rule;
@@ -57,6 +59,8 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase {
@Mock
private LayoutInflater mLayoutInflater;
@Mock
private AsyncLayoutInflater mAsyncLayoutInflater;
@Mock
private KeyguardInputViewController.Factory mKeyguardSecurityViewControllerFactory;
@Mock
private EmergencyButtonController.Factory mEmergencyButtonControllerFactory;
@@ -70,6 +74,8 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase {
private WindowInsetsController mWindowInsetsController;
@Mock
private KeyguardSecurityCallback mKeyguardSecurityCallback;
@Mock
private FeatureFlags mFeatureFlags;
private KeyguardSecurityViewFlipperController mKeyguardSecurityViewFlipperController;
@@ -82,10 +88,11 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase {
when(mView.getWindowInsetsController()).thenReturn(mWindowInsetsController);
when(mEmergencyButtonControllerFactory.create(any(EmergencyButton.class)))
.thenReturn(mEmergencyButtonController);
when(mView.getContext()).thenReturn(getContext());
mKeyguardSecurityViewFlipperController = new KeyguardSecurityViewFlipperController(mView,
mLayoutInflater, mKeyguardSecurityViewControllerFactory,
mEmergencyButtonControllerFactory);
mLayoutInflater, mAsyncLayoutInflater, mKeyguardSecurityViewControllerFactory,
mEmergencyButtonControllerFactory, mFeatureFlags);
}
@Test
@@ -107,6 +114,14 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase {
}
}
@Test
public void asynchronouslyInflateView() {
mKeyguardSecurityViewFlipperController.asynchronouslyInflateView(SecurityMode.PIN,
mKeyguardSecurityCallback, null);
verify(mAsyncLayoutInflater).inflate(anyInt(), eq(mView), any(
AsyncLayoutInflater.OnInflateFinishedListener.class));
}
@Test
public void onDensityOrFontScaleChanged() {
mKeyguardSecurityViewFlipperController.clearViews();