Merge "Show a small toast when a double tap is needed." into sc-dev

This commit is contained in:
Dave Mankoff
2021-05-26 16:03:52 +00:00
committed by Android (Google) Code Review
9 changed files with 319 additions and 13 deletions

View File

@@ -129,7 +129,21 @@
android:layout_marginTop="@dimen/status_bar_header_height_keyguard"
android:text="@string/report_rejected_touch"
android:visibility="gone" />
<com.android.systemui.statusbar.phone.TapAgainView
android:id="@+id/shade_falsing_tap_again"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
systemui:layout_constraintLeft_toLeftOf="parent"
systemui:layout_constraintRight_toRightOf="parent"
systemui:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="20dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:elevation="10dp"
android:visibility="gone"
/>
</com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer>
<FrameLayout

View File

@@ -40,6 +40,7 @@ public class FalsingManagerFake implements FalsingManager {
private boolean mIsFalseRobustTap;
private final List<FalsingBeliefListener> mFalsingBeliefListeners = new ArrayList<>();
private final List<FalsingTapListener> mTapListeners = new ArrayList<>();
@Override
public void onSuccessfulUnlock() {
@@ -148,11 +149,15 @@ public class FalsingManagerFake implements FalsingManager {
@Override
public void addTapListener(FalsingTapListener falsingTapListener) {
mTapListeners.add(falsingTapListener);
}
@Override
public void removeTapListener(FalsingTapListener falsingTapListener) {
mTapListeners.remove(falsingTapListener);
}
public List<FalsingTapListener> getTapListeners() {
return mTapListeners;
}
}

View File

@@ -142,7 +142,6 @@ public class KeyguardIndicationTextView extends TextView {
Animator yTranslate =
ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, 0, -getYTranslationPixels());
yTranslate.setDuration(getFadeOutDuration());
fadeOut.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
animatorSet.playTogether(fadeOut, yTranslate);
return animatorSet;

View File

@@ -24,6 +24,8 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import com.android.systemui.R;
public class NotificationPanelView extends PanelView {
private static final boolean DEBUG = false;
@@ -92,6 +94,10 @@ public class NotificationPanelView extends PanelView {
mRtlChangeListener = listener;
}
public TapAgainView getTapAgainView() {
return findViewById(R.id.shade_falsing_tap_again);
}
interface RtlChangeListener {
void onRtlPropertielsChanged(int layoutDirection);
}

View File

@@ -97,8 +97,8 @@ import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.media.KeyguardMediaController;
import com.android.systemui.media.MediaDataManager;
import com.android.systemui.media.MediaHierarchyManager;
@@ -306,6 +306,7 @@ public class NotificationPanelViewController extends PanelViewController {
private final KeyguardUserSwitcherComponent.Factory mKeyguardUserSwitcherComponentFactory;
private final KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory;
private final QSDetailDisplayer mQSDetailDisplayer;
private final FragmentService mFragmentService;
private final FeatureFlags mFeatureFlags;
private final ScrimController mScrimController;
private final PrivacyDotViewController mPrivacyDotViewController;
@@ -314,6 +315,7 @@ public class NotificationPanelViewController extends PanelViewController {
// If there are exactly 1 + mMaxKeyguardNotifications, then still shows all notifications
private final int mMaxKeyguardNotifications;
private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
private final TapAgainViewController mTapAgainViewController;
private boolean mShouldUseSplitNotificationShade;
// Current max allowed keyguard notifications determined by measuring the panel
private int mMaxAllowedKeyguardNotifications;
@@ -604,7 +606,12 @@ public class NotificationPanelViewController extends PanelViewController {
private final FalsingTapListener mFalsingTapListener = new FalsingTapListener() {
@Override
public void onDoubleTapRequired() {
showTransientIndication(R.string.notification_tap_again);
if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
mTapAgainViewController.show();
} else {
mKeyguardIndicationController.showTransientIndication(
R.string.notification_tap_again);
}
mVibratorHelper.vibrate(VibrationEffect.EFFECT_STRENGTH_MEDIUM);
}
};
@@ -653,6 +660,8 @@ public class NotificationPanelViewController extends PanelViewController {
QuickAccessWalletClient quickAccessWalletClient,
KeyguardMediaController keyguardMediaController,
PrivacyDotViewController privacyDotViewController,
TapAgainViewController tapAgainViewController,
FragmentService fragmentService,
@Main Executor uiExecutor,
SecureSettings secureSettings) {
super(view, falsingManager, dozeLog, keyguardStateController,
@@ -679,6 +688,7 @@ public class NotificationPanelViewController extends PanelViewController {
mKeyguardQsUserSwitchComponentFactory = keyguardQsUserSwitchComponentFactory;
mKeyguardUserSwitcherComponentFactory = keyguardUserSwitcherComponentFactory;
mQSDetailDisplayer = qsDetailDisplayer;
mFragmentService = fragmentService;
mKeyguardUserSwitcherEnabled = mResources.getBoolean(
com.android.internal.R.bool.config_keyguardUserSwitcher);
mKeyguardQsUserSwitchEnabled =
@@ -705,6 +715,7 @@ public class NotificationPanelViewController extends PanelViewController {
mUserManager = userManager;
mMediaDataManager = mediaDataManager;
mQuickAccessWalletClient = quickAccessWalletClient;
mTapAgainViewController = tapAgainViewController;
mUiExecutor = uiExecutor;
mSecureSettings = secureSettings;
pulseExpansionHandler.setPulseExpandAbortListener(() -> {
@@ -843,6 +854,8 @@ public class NotificationPanelViewController extends PanelViewController {
if (mShouldUseSplitNotificationShade) {
updateResources();
}
mTapAgainViewController.init();
}
@Override
@@ -3675,10 +3688,6 @@ public class NotificationPanelViewController extends PanelViewController {
updateMaxDisplayedNotifications(true);
}
public void showTransientIndication(int id) {
mKeyguardIndicationController.showTransientIndication(id);
}
public void setAlpha(float alpha) {
mView.setAlpha(alpha);
}
@@ -4264,7 +4273,8 @@ public class NotificationPanelViewController extends PanelViewController {
private class OnAttachStateChangeListener implements View.OnAttachStateChangeListener {
@Override
public void onViewAttachedToWindow(View v) {
FragmentHostManager.get(mView).addTagListener(QS.TAG, mFragmentListener);
mFragmentService.getFragmentHostManager(mView)
.addTagListener(QS.TAG, mFragmentListener);
mStatusBarStateController.addCallback(mStatusBarStateListener);
mConfigurationController.addCallback(mConfigurationListener);
mUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
@@ -4278,7 +4288,8 @@ public class NotificationPanelViewController extends PanelViewController {
@Override
public void onViewDetachedFromWindow(View v) {
FragmentHostManager.get(mView).removeTagListener(QS.TAG, mFragmentListener);
mFragmentService.getFragmentHostManager(mView)
.removeTagListener(QS.TAG, mFragmentListener);
mStatusBarStateController.removeCallback(mStatusBarStateListener);
mConfigurationController.removeCallback(mConfigurationListener);
mUpdateMonitor.removeCallback(mKeyguardUpdateCallback);

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2021 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.systemui.statusbar.phone;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.systemui.R;
import com.android.wm.shell.animation.Interpolators;
/**
* View to show a toast-like popup on the notification shade and quick settings.
*/
public class TapAgainView extends FrameLayout {
public TapAgainView(
@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
updateBgColor();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
TextView text = new TextView(mContext);
text.setText(R.string.notification_tap_again);
addView(text);
}
void updateBgColor() {
setBackgroundResource(R.drawable.rounded_bg_full);
}
/** Make the view visible. */
public void animateIn() {
int yTranslation = mContext.getResources().getDimensionPixelSize(
R.dimen.keyguard_indication_y_translation);
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(this, View.ALPHA, 1f);
fadeIn.setStartDelay(150); // From KeyguardIndicationTextView#getFadeInDelay
fadeIn.setDuration(317); // From KeyguardIndicationTextView#getFadeInDuration
fadeIn.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
Animator yTranslate =
ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, yTranslation, 0);
yTranslate.setDuration(600); // From KeyguardIndicationTextView#getYInDuration
yTranslate.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
setTranslationY(0);
}
});
animatorSet.playTogether(yTranslate, fadeIn);
animatorSet.start();
setVisibility(View.VISIBLE);
}
/** Make the view gone. */
public void animateOut() {
long fadeOutDuration = 167L; // From KeyguardIndicationTextView#getFadeOutDuration
int yTranslation = mContext.getResources().getDimensionPixelSize(
com.android.systemui.R.dimen.keyguard_indication_y_translation);
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(this, View.ALPHA, 0f);
fadeOut.setDuration(fadeOutDuration);
fadeOut.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
Animator yTranslate =
ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, 0, -yTranslation);
yTranslate.setDuration(fadeOutDuration);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setVisibility(GONE);
}
@Override
public void onAnimationCancel(Animator animation) {
setVisibility(GONE);
}
});
animatorSet.playTogether(yTranslate, fadeOut);
animatorSet.start();
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2021 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.systemui.statusbar.phone;
import static com.android.systemui.classifier.FalsingModule.DOUBLE_TAP_TIMEOUT_MS;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.concurrency.DelayableExecutor;
import javax.inject.Inject;
import javax.inject.Named;
/**
* Controller for {@link TapAgainView}.
*/
@StatusBarComponent.StatusBarScope
public class TapAgainViewController extends ViewController<TapAgainView> {
private final DelayableExecutor mDelayableExecutor;
private final ConfigurationController mConfigurationController;
private final long mDoubleTapTimeMs;
private Runnable mHideCanceler;
@VisibleForTesting
final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
@Override
public void onOverlayChanged() {
mView.updateBgColor();
}
@Override
public void onUiModeChanged() {
mView.updateBgColor();
}
@Override
public void onThemeChanged() {
mView.updateBgColor();
}
};
@Inject
protected TapAgainViewController(TapAgainView view,
@Main DelayableExecutor delayableExecutor,
ConfigurationController configurationController,
@Named(DOUBLE_TAP_TIMEOUT_MS) long doubleTapTimeMs) {
super(view);
mDelayableExecutor = delayableExecutor;
mConfigurationController = configurationController;
mDoubleTapTimeMs = doubleTapTimeMs;
}
@Override
protected void onViewAttached() {
mConfigurationController.addCallback(mConfigurationListener);
}
@Override
protected void onViewDetached() {
mConfigurationController.removeCallback(mConfigurationListener);
}
/** Shows the associated view, possibly animating it. */
public void show() {
if (mHideCanceler != null) {
mHideCanceler.run();
}
mView.animateIn();
mHideCanceler = mDelayableExecutor.executeDelayed(this::hide, mDoubleTapTimeMs);
}
/** Hides the associated view, possibly animating it. */
public void hide() {
mHideCanceler = null;
mView.animateOut();
}
}

View File

@@ -23,6 +23,7 @@ import com.android.systemui.R;
import com.android.systemui.biometrics.AuthRippleView;
import com.android.systemui.statusbar.phone.NotificationPanelView;
import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
import com.android.systemui.statusbar.phone.TapAgainView;
import dagger.Module;
import dagger.Provides;
@@ -53,4 +54,11 @@ public abstract class StatusBarViewModule {
NotificationShadeWindowView notificationShadeWindowView) {
return notificationShadeWindowView.findViewById(R.id.auth_ripple);
}
/** */
@Provides
@StatusBarComponent.StatusBarScope
public static TapAgainView getTapAgainView(NotificationPanelView npv) {
return npv.getTapAgainView();
}
}

View File

@@ -20,12 +20,15 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
import static com.android.systemui.statusbar.notification.ViewGroupFadeHelper.reset;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -75,13 +78,17 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.media.KeyguardMediaController;
import com.android.systemui.media.MediaDataManager;
import com.android.systemui.media.MediaHierarchyManager;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.qs.QSDetailDisplayer;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.KeyguardAffordanceView;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -252,11 +259,21 @@ public class NotificationPanelViewTest extends SysuiTestCase {
private PrivacyDotViewController mPrivacyDotViewController;
@Mock
private SecureSettings mSecureSettings;
@Mock
private TapAgainViewController mTapAgainViewController;
@Mock
private KeyguardIndicationController mKeyguardIndicationController;
@Mock
private FragmentService mFragmentService;
@Mock
private FragmentHostManager mFragmentHostManager;
private SysuiStatusBarStateController mStatusBarStateController;
private NotificationPanelViewController mNotificationPanelViewController;
private View.AccessibilityDelegate mAccessibiltyDelegate;
private NotificationsQuickSettingsContainer mNotificationContainerParent;
private List<View.OnAttachStateChangeListener> mOnAttachStateChangeListeners;
private FalsingManagerFake mFalsingManager = new FalsingManagerFake();
@Before
public void setup() {
@@ -297,6 +314,7 @@ public class NotificationPanelViewTest extends SysuiTestCase {
mNotificationContainerParent.addView(newViewWithId(R.id.keyguard_status_view));
when(mView.findViewById(R.id.notification_container_parent))
.thenReturn(mNotificationContainerParent);
when(mFragmentService.getFragmentHostManager(mView)).thenReturn(mFragmentHostManager);
FlingAnimationUtils.Builder flingAnimationUtilsBuilder = new FlingAnimationUtils.Builder(
mDisplayMetrics);
@@ -317,7 +335,7 @@ public class NotificationPanelViewTest extends SysuiTestCase {
mKeyguardBypassController, mHeadsUpManager,
mock(NotificationRoundnessManager.class),
mStatusBarStateController,
new FalsingManagerFake(),
mFalsingManager,
mLockscreenShadeTransitionController,
new FalsingCollectorFake());
when(mKeyguardStatusViewComponentFactory.build(any()))
@@ -331,11 +349,12 @@ public class NotificationPanelViewTest extends SysuiTestCase {
when(mKeyguardStatusBarViewComponent.getKeyguardStatusBarViewController())
.thenReturn(mKeyguardStatusBarViewController);
reset(mView);
mNotificationPanelViewController = new NotificationPanelViewController(mView,
mResources,
mLayoutInflater,
coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController,
new FalsingManagerFake(), new FalsingCollectorFake(),
mFalsingManager, new FalsingCollectorFake(),
mNotificationLockscreenUserManager, mNotificationEntryManager,
mKeyguardStateController, mStatusBarStateController, mDozeLog,
mDozeParameters, mCommandQueue, mVibratorHelper,
@@ -364,6 +383,8 @@ public class NotificationPanelViewTest extends SysuiTestCase {
mQuickAccessWalletClient,
mKeyguardMediaController,
mPrivacyDotViewController,
mTapAgainViewController,
mFragmentService,
new FakeExecutor(new FakeSystemClock()),
mSecureSettings);
mNotificationPanelViewController.initDependencies(
@@ -371,6 +392,13 @@ public class NotificationPanelViewTest extends SysuiTestCase {
mNotificationShelfController);
mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
mNotificationPanelViewController.setBar(mPanelBar);
mNotificationPanelViewController.setKeyguardIndicationController(
mKeyguardIndicationController);
ArgumentCaptor<View.OnAttachStateChangeListener> onAttachStateChangeListenerArgumentCaptor =
ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
verify(mView, atLeast(1)).addOnAttachStateChangeListener(
onAttachStateChangeListenerArgumentCaptor.capture());
mOnAttachStateChangeListeners = onAttachStateChangeListenerArgumentCaptor.getAllValues();
ArgumentCaptor<View.AccessibilityDelegate> accessibilityDelegateArgumentCaptor =
ArgumentCaptor.forClass(View.AccessibilityDelegate.class);
@@ -616,6 +644,34 @@ public class NotificationPanelViewTest extends SysuiTestCase {
verify(mKeyguardStateController).notifyPanelFlingEnd();
}
@Test
public void testDoubleTapRequired_Keyguard() {
FalsingManager.FalsingTapListener listener = getFalsingTapListener();
mStatusBarStateController.setState(KEYGUARD);
listener.onDoubleTapRequired();
verify(mKeyguardIndicationController).showTransientIndication(anyInt());
}
@Test
public void testDoubleTapRequired_ShadeLocked() {
FalsingManager.FalsingTapListener listener = getFalsingTapListener();
mStatusBarStateController.setState(SHADE_LOCKED);
listener.onDoubleTapRequired();
verify(mTapAgainViewController).show();
}
private FalsingManager.FalsingTapListener getFalsingTapListener() {
for (View.OnAttachStateChangeListener listener : mOnAttachStateChangeListeners) {
listener.onViewAttachedToWindow(mView);
}
assertThat(mFalsingManager.getTapListeners().size()).isEqualTo(1);
return mFalsingManager.getTapListeners().get(0);
}
private View newViewWithId(int id) {
View view = new View(mContext);
view.setId(id);