diff options
Diffstat (limited to 'packages/SystemUI/src')
242 files changed, 6726 insertions, 15639 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index a65accf1ff15..4ffd22c73116 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -107,7 +107,7 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey if (shouldLockout(deadline)) { handleAttemptLockout(deadline); } else { - mView.resetState(); + resetState(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 5ad8cad8195a..272954df6dd6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -104,6 +104,8 @@ public class KeyguardClockSwitch extends RelativeLayout { private boolean mSupportsDarkText; private int[] mColorPalette; + private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL; + public KeyguardClockSwitch(Context context, AttributeSet attrs) { super(context, attrs); @@ -128,6 +130,35 @@ public class KeyguardClockSwitch extends RelativeLayout { return mClockPlugin != null; } + /** + * Update lock screen mode for testing different layouts + */ + public void updateLockScreenMode(int mode) { + mLockScreenMode = mode; + RelativeLayout.LayoutParams statusAreaLP = (RelativeLayout.LayoutParams) + mKeyguardStatusArea.getLayoutParams(); + RelativeLayout.LayoutParams clockLP = (RelativeLayout.LayoutParams) + mSmallClockFrame.getLayoutParams(); + + if (mode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) { + statusAreaLP.removeRule(RelativeLayout.BELOW); + statusAreaLP.addRule(RelativeLayout.LEFT_OF, R.id.clock_view); + statusAreaLP.addRule(RelativeLayout.ALIGN_PARENT_START); + + clockLP.addRule(RelativeLayout.ALIGN_PARENT_END); + clockLP.width = ViewGroup.LayoutParams.WRAP_CONTENT; + } else { + statusAreaLP.removeRule(RelativeLayout.LEFT_OF); + statusAreaLP.removeRule(RelativeLayout.ALIGN_PARENT_START); + statusAreaLP.addRule(RelativeLayout.BELOW, R.id.clock_view); + + clockLP.removeRule(RelativeLayout.ALIGN_PARENT_END); + clockLP.width = ViewGroup.LayoutParams.MATCH_PARENT; + } + + requestLayout(); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -363,6 +394,10 @@ public class KeyguardClockSwitch extends RelativeLayout { * these cases. */ void setKeyguardShowingHeader(boolean hasHeader) { + if (mLockScreenMode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL) { + hasHeader = false; + } + if (mShowingHeader == hasHeader) { return; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java index 9ffa658da0e8..351369c51364 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java @@ -163,16 +163,18 @@ public class KeyguardHostViewController extends ViewController<KeyguardHostView> @Inject public KeyguardHostViewController(KeyguardHostView view, KeyguardUpdateMonitor keyguardUpdateMonitor, - KeyguardSecurityContainerController keyguardSecurityContainerController, AudioManager audioManager, TelephonyManager telephonyManager, - ViewMediatorCallback viewMediatorCallback) { + ViewMediatorCallback viewMediatorCallback, + KeyguardSecurityContainerController.Factory + keyguardSecurityContainerControllerFactory) { super(view); mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mKeyguardSecurityContainerController = keyguardSecurityContainerController; mAudioManager = audioManager; mTelephonyManager = telephonyManager; mViewMediatorCallback = viewMediatorCallback; + mKeyguardSecurityContainerController = keyguardSecurityContainerControllerFactory.create( + mSecurityCallback); } /** Initialize the Controller. */ @@ -188,7 +190,6 @@ public class KeyguardHostViewController extends ViewController<KeyguardHostView> mViewMediatorCallback.setNeedsInput(mKeyguardSecurityContainerController.needsInput()); mKeyguardUpdateMonitor.registerCallback(mUpdateCallback); mView.setOnKeyListener(mOnKeyListener); - mKeyguardSecurityContainerController.setSecurityCallback(mSecurityCallback); mKeyguardSecurityContainerController.showPrimarySecurityScreen(false); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 64676e55b038..1c23605a8516 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -68,8 +68,8 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final UiEventLogger mUiEventLogger; private final KeyguardStateController mKeyguardStateController; private final KeyguardSecurityViewFlipperController mSecurityViewFlipperController; + private final SecurityCallback mSecurityCallback; - private SecurityCallback mSecurityCallback; private SecurityMode mCurrentSecurityMode = SecurityMode.Invalid; private KeyguardSecurityCallback mKeyguardSecurityCallback = new KeyguardSecurityCallback() { @@ -145,8 +145,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } }; - @Inject - KeyguardSecurityContainerController(KeyguardSecurityContainer view, + private KeyguardSecurityContainerController(KeyguardSecurityContainer view, AdminSecondaryLockScreenController.Factory adminSecondaryLockScreenControllerFactory, LockPatternUtils lockPatternUtils, KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -154,6 +153,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard MetricsLogger metricsLogger, UiEventLogger uiEventLogger, KeyguardStateController keyguardStateController, + SecurityCallback securityCallback, KeyguardSecurityViewFlipperController securityViewFlipperController) { super(view); mLockPatternUtils = lockPatternUtils; @@ -162,6 +162,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mMetricsLogger = metricsLogger; mUiEventLogger = uiEventLogger; mKeyguardStateController = keyguardStateController; + mSecurityCallback = securityCallback; mSecurityViewFlipperController = securityViewFlipperController; mAdminSecondaryLockScreenController = adminSecondaryLockScreenControllerFactory.create( mKeyguardSecurityCallback); @@ -269,10 +270,6 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } - public void setSecurityCallback(SecurityCallback securityCallback) { - mSecurityCallback = securityCallback; - } - /** * Shows the next security screen if there is one. * @param authenticated true if the user entered the correct authentication @@ -450,4 +447,49 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mCurrentSecurityMode = securityMode; return getCurrentSecurityController(); } + + static class Factory { + + private final KeyguardSecurityContainer mView; + private final AdminSecondaryLockScreenController.Factory + mAdminSecondaryLockScreenControllerFactory; + private final LockPatternUtils mLockPatternUtils; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final KeyguardSecurityModel mKeyguardSecurityModel; + private final MetricsLogger mMetricsLogger; + private final UiEventLogger mUiEventLogger; + private final KeyguardStateController mKeyguardStateController; + private final KeyguardSecurityViewFlipperController mSecurityViewFlipperController; + + @Inject + Factory(KeyguardSecurityContainer view, + AdminSecondaryLockScreenController.Factory + adminSecondaryLockScreenControllerFactory, + LockPatternUtils lockPatternUtils, + KeyguardUpdateMonitor keyguardUpdateMonitor, + KeyguardSecurityModel keyguardSecurityModel, + MetricsLogger metricsLogger, + UiEventLogger uiEventLogger, + KeyguardStateController keyguardStateController, + KeyguardSecurityViewFlipperController securityViewFlipperController) { + mView = view; + mAdminSecondaryLockScreenControllerFactory = adminSecondaryLockScreenControllerFactory; + mLockPatternUtils = lockPatternUtils; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mKeyguardSecurityModel = keyguardSecurityModel; + mMetricsLogger = metricsLogger; + mUiEventLogger = uiEventLogger; + mKeyguardStateController = keyguardStateController; + mSecurityViewFlipperController = securityViewFlipperController; + } + + public KeyguardSecurityContainerController create( + SecurityCallback securityCallback) { + return new KeyguardSecurityContainerController(mView, + mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils, + mKeyguardUpdateMonitor, mKeyguardSecurityModel, mMetricsLogger, mUiEventLogger, + mKeyguardStateController, securityCallback, mSecurityViewFlipperController); + } + + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java index 6e111745627f..9ef2def04ec1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -79,6 +79,11 @@ public class KeyguardStatusView extends GridLayout implements private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { @Override + public void onLockScreenModeChanged(int mode) { + updateLockScreenMode(mode); + } + + @Override public void onTimeChanged() { refreshTime(); } @@ -255,6 +260,10 @@ public class KeyguardStatusView extends GridLayout implements mClockView.refresh(); } + private void updateLockScreenMode(int mode) { + mClockView.updateLockScreenMode(mode); + } + private void updateTimeZone(TimeZone timeZone) { mClockView.onTimeZoneChanged(timeZone); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index a8dd03f668e2..bb8a99bb8cd8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -56,7 +56,7 @@ import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.face.FaceManager; -import android.hardware.face.FaceSensorProperties; +import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback; import android.hardware.fingerprint.FingerprintManager.AuthenticationResult; @@ -100,8 +100,8 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.util.Assert; @@ -181,6 +181,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private static final int MSG_USER_STOPPED = 340; private static final int MSG_USER_REMOVED = 341; private static final int MSG_KEYGUARD_GOING_AWAY = 342; + private static final int MSG_LOCK_SCREEN_MODE = 343; + + public static final int LOCK_SCREEN_MODE_NORMAL = 0; + public static final int LOCK_SCREEN_MODE_LAYOUT_1 = 1; /** Biometric authentication state: Not listening. */ private static final int BIOMETRIC_STATE_STOPPED = 0; @@ -264,6 +268,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>> mCallbacks = Lists.newArrayList(); private ContentObserver mDeviceProvisionedObserver; + private ContentObserver mLockScreenModeObserver; private boolean mSwitchingUser; @@ -287,6 +292,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean mLockIconPressed; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private final Executor mBackgroundExecutor; + private int mLockScreenMode; /** * Short delay before restarting fingerprint authentication after a successful try. This should @@ -1337,7 +1343,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private CancellationSignal mFaceCancelSignal; private FingerprintManager mFpm; private FaceManager mFaceManager; - private List<FaceSensorProperties> mFaceSensorProperties; + private List<FaceSensorPropertiesInternal> mFaceSensorProperties; private boolean mFingerprintLockedOut; private TelephonyManager mTelephonyManager; @@ -1695,6 +1701,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab case MSG_KEYGUARD_GOING_AWAY: handleKeyguardGoingAway((boolean) msg.obj); break; + case MSG_LOCK_SCREEN_MODE: + handleLockScreenMode(); + break; default: super.handleMessage(msg); break; @@ -1779,7 +1788,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) { mFaceManager = (FaceManager) context.getSystemService(Context.FACE_SERVICE); - mFaceSensorProperties = mFaceManager.getSensorProperties(); + mFaceSensorProperties = mFaceManager.getSensorPropertiesInternal(); } if (mFpm != null || mFaceManager != null) { @@ -1797,7 +1806,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mIsAutomotive = isAutomotive(); - ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); + TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); mUserManager = context.getSystemService(UserManager.class); mIsPrimaryUser = mUserManager.isPrimaryUser(); int user = ActivityManager.getCurrentUser(); @@ -1829,6 +1838,23 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } } + + updateLockScreenMode(); + mLockScreenModeObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + updateLockScreenMode(); + mHandler.sendEmptyMessage(MSG_LOCK_SCREEN_MODE); + } + }; + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(Settings.Global.SHOW_NEW_LOCKSCREEN), + false, mLockScreenModeObserver); + } + + private void updateLockScreenMode() { + mLockScreenMode = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.SHOW_NEW_LOCKSCREEN, 0); } private final UserSwitchObserver mUserSwitchObserver = new UserSwitchObserver() { @@ -2351,6 +2377,20 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * Handle {@link #MSG_LOCK_SCREEN_MODE} + */ + private void handleLockScreenMode() { + Assert.isMainThread(); + if (DEBUG) Log.d(TAG, "handleLockScreenMode(" + mLockScreenMode + ")"); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onLockScreenModeChanged(mLockScreenMode); + } + } + } + + /** * Handle (@line #MSG_TIMEZONE_UPDATE} */ private void handleTimeZoneUpdate(String timeZone) { @@ -2679,6 +2719,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab callback.onClockVisibilityChanged(); callback.onKeyguardVisibilityChangedRaw(mKeyguardIsVisible); callback.onTelephonyCapable(mTelephonyCapable); + callback.onLockScreenModeChanged(mLockScreenMode); + for (Entry<Integer, SimData> data : mSimDatas.entrySet()) { final SimData state = data.getValue(); callback.onSimStateChanged(state.subId, state.slotId, state.simState); @@ -3023,13 +3065,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver); } + if (mLockScreenModeObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mLockScreenModeObserver); + } + try { ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver); } catch (RemoteException e) { Log.d(TAG, "RemoteException onDestroy. cannot unregister userSwitchObserver"); } - ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); + TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver); mBroadcastDispatcher.unregisterReceiver(mBroadcastAllReceiver); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 37c66639876e..9c2b14945cc2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -325,4 +325,9 @@ public class KeyguardUpdateMonitorCallback { */ public void onSecondaryLockscreenRequirementChanged(int userId) { } + /** + * Called to switch lock screen layout/clock layouts + */ + public void onLockScreenModeChanged(int mode) { } + } diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 832edf719dbb..9f28e0936d7f 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -37,7 +37,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.appops.AppOpsController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -47,6 +47,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; @@ -126,6 +127,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.SystemWindows; +import java.util.concurrent.Executor; import java.util.function.Consumer; import javax.inject.Inject; @@ -167,6 +169,15 @@ public class Dependency { * Generic handler on the main thread. */ private static final String MAIN_HANDLER_NAME = "main_handler"; + /** + * Generic executor on the main thread. + */ + private static final String MAIN_EXECUTOR_NAME = "main_executor"; + + /** + * Generic executor on a background thread. + */ + private static final String BACKGROUND_EXECUTOR_NAME = "background_executor"; /** * An email address to send memory leak reports to by default. @@ -199,6 +210,17 @@ public class Dependency { new DependencyKey<>(MAIN_HANDLER_NAME); /** + * Generic executor on the main thread. + */ + public static final DependencyKey<Executor> MAIN_EXECUTOR = + new DependencyKey<>(MAIN_EXECUTOR_NAME); + /** + * Generic executor on a background thread. + */ + public static final DependencyKey<Executor> BACKGROUND_EXECUTOR = + new DependencyKey<>(BACKGROUND_EXECUTOR_NAME); + + /** * An email address to send memory leak reports to by default. */ public static final DependencyKey<String> LEAK_REPORT_EMAIL = @@ -288,7 +310,7 @@ public class Dependency { @Inject Lazy<KeyguardDismissUtil> mKeyguardDismissUtil; @Inject Lazy<SmartReplyController> mSmartReplyController; @Inject Lazy<RemoteInputQuickSettingsDisabler> mRemoteInputQuickSettingsDisabler; - @Inject Lazy<BubbleController> mBubbleController; + @Inject Lazy<Bubbles> mBubbles; @Inject Lazy<NotificationEntryManager> mNotificationEntryManager; @Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager; @Inject Lazy<AutoHideController> mAutoHideController; @@ -301,6 +323,8 @@ public class Dependency { @Inject @Named(TIME_TICK_HANDLER_NAME) Lazy<Handler> mTimeTickHandler; @Nullable @Inject @Named(LEAK_REPORT_EMAIL_NAME) Lazy<String> mLeakReportEmail; + @Inject @Main Lazy<Executor> mMainExecutor; + @Inject @Background Lazy<Executor> mBackgroundExecutor; @Inject Lazy<ClockManager> mClockManager; @Inject Lazy<ActivityManagerWrapper> mActivityManagerWrapper; @Inject Lazy<DevicePolicyManagerWrapper> mDevicePolicyManagerWrapper; @@ -321,6 +345,7 @@ public class Dependency { @Inject Lazy<DisplayImeController> mDisplayImeController; @Inject Lazy<RecordingController> mRecordingController; @Inject Lazy<ProtoTracer> mProtoTracer; + @Inject Lazy<MediaOutputDialogFactory> mMediaOutputDialogFactory; @Inject public Dependency() { @@ -336,6 +361,8 @@ public class Dependency { mProviders.put(BG_LOOPER, mBgLooper::get); mProviders.put(MAIN_LOOPER, mMainLooper::get); mProviders.put(MAIN_HANDLER, mMainHandler::get); + mProviders.put(MAIN_EXECUTOR, mMainExecutor::get); + mProviders.put(BACKGROUND_EXECUTOR, mBackgroundExecutor::get); mProviders.put(ActivityStarter.class, mActivityStarter::get); mProviders.put(BroadcastDispatcher.class, mBroadcastDispatcher::get); @@ -483,7 +510,7 @@ public class Dependency { mProviders.put(SmartReplyController.class, mSmartReplyController::get); mProviders.put(RemoteInputQuickSettingsDisabler.class, mRemoteInputQuickSettingsDisabler::get); - mProviders.put(BubbleController.class, mBubbleController::get); + mProviders.put(Bubbles.class, mBubbles::get); mProviders.put(NotificationEntryManager.class, mNotificationEntryManager::get); mProviders.put(ForegroundServiceNotificationListener.class, mForegroundServiceNotificationListener::get); @@ -516,6 +543,8 @@ public class Dependency { mProviders.put(RecordingController.class, mRecordingController::get); + mProviders.put(MediaOutputDialogFactory.class, mMediaOutputDialogFactory::get); + Dependency.setInstance(this); } diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index f4c865e1d131..f210d508907c 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -72,11 +72,11 @@ public final class Prefs { Key.QS_HAS_TURNED_OFF_MOBILE_DATA, Key.TOUCHED_RINGER_TOGGLE, Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, - Key.HAS_SEEN_BUBBLES_EDUCATION, - Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION, Key.HAS_SEEN_REVERSE_BOTTOM_SHEET, - Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT + Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT, + Key.HAS_SEEN_PRIORITY_ONBOARDING }) + // TODO: annotate these with their types so {@link PrefsCommandLine} can know how to set them public @interface Key { @Deprecated String OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME = "OverviewLastStackTaskActiveTime"; @@ -121,8 +121,6 @@ public final class Prefs { String QS_HAS_TURNED_OFF_MOBILE_DATA = "QsHasTurnedOffMobileData"; String TOUCHED_RINGER_TOGGLE = "TouchedRingerToggle"; String HAS_SEEN_ODI_CAPTIONS_TOOLTIP = "HasSeenODICaptionsTooltip"; - String HAS_SEEN_BUBBLES_EDUCATION = "HasSeenBubblesOnboarding"; - String HAS_SEEN_BUBBLES_MANAGE_EDUCATION = "HasSeenBubblesManageOnboarding"; String HAS_SEEN_REVERSE_BOTTOM_SHEET = "HasSeenReverseBottomSheet"; String CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT = "ControlsStructureSwipeTooltipCount"; /** Tracks whether the user has seen the onboarding screen for priority conversations */ diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index fbfabd1e4ae7..a4648ee75485 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -86,6 +86,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.SecureSetting; +import com.android.systemui.settings.UserTracker; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -125,6 +126,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { private final TunerService mTunerService; private DisplayManager.DisplayListener mDisplayListener; private CameraAvailabilityListener mCameraListener; + private final UserTracker mUserTracker; //TODO: These are piecemeal being updated to Points for now to support non-square rounded // corners. for now it is only supposed when reading the intrinsic size from the drawables with @@ -202,11 +204,13 @@ public class ScreenDecorations extends SystemUI implements Tunable { public ScreenDecorations(Context context, @Main Handler handler, BroadcastDispatcher broadcastDispatcher, - TunerService tunerService) { + TunerService tunerService, + UserTracker userTracker) { super(context); mMainHandler = handler; mBroadcastDispatcher = broadcastDispatcher; mTunerService = tunerService; + mUserTracker = userTracker; } @Override @@ -310,7 +314,8 @@ public class ScreenDecorations extends SystemUI implements Tunable { // Watch color inversion and invert the overlay as needed. if (mColorInversionSetting == null) { mColorInversionSetting = new SecureSetting(mContext, mHandler, - Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) { + Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, + mUserTracker.getUserId()) { @Override protected void handleValueChanged(int value, boolean observedChange) { updateColorInversion(value); diff --git a/packages/SystemUI/src/com/android/systemui/SizeCompatModeActivityController.java b/packages/SystemUI/src/com/android/systemui/SizeCompatModeActivityController.java index 34efa35c37c5..02f34ac3dec0 100644 --- a/packages/SystemUI/src/com/android/systemui/SizeCompatModeActivityController.java +++ b/packages/SystemUI/src/com/android/systemui/SizeCompatModeActivityController.java @@ -42,8 +42,8 @@ import android.widget.PopupWindow; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.CommandQueue; import java.lang.ref.WeakReference; @@ -66,11 +66,11 @@ public class SizeCompatModeActivityController extends SystemUI implements Comman @VisibleForTesting @Inject - SizeCompatModeActivityController(Context context, ActivityManagerWrapper am, + SizeCompatModeActivityController(Context context, TaskStackChangeListeners listeners, CommandQueue commandQueue) { super(context); mCommandQueue = commandQueue; - am.registerTaskStackListener(new TaskStackChangeListener() { + listeners.registerTaskStackListener(new TaskStackChangeListener() { @Override public void onSizeCompatModeActivityChanged(int displayId, IBinder activityToken) { // Note the callback already runs on main thread. diff --git a/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java b/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java index 2365f128532e..47adffc216a5 100644 --- a/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java @@ -111,9 +111,7 @@ public class SlicePermissionActivity extends Activity implements OnClickListener final String providerPkg = getIntent().getStringExtra("provider_pkg"); if (providerPkg == null || mProviderPkg.equals(providerPkg)) return; final String callingPkg = getCallingPkg(); - EventLog.writeEvent(0x534e4554, "159145361", getUid(callingPkg), String.format( - "pkg %s (disguised as %s) attempted to request permission to show %s slices in %s", - callingPkg, providerPkg, mProviderPkg, mCallingPkg)); + EventLog.writeEvent(0x534e4554, "159145361", getUid(callingPkg)); } @Nullable diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java index 68e404e36bba..69a0d65a6963 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java @@ -16,11 +16,14 @@ package com.android.systemui.accessibility; +import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; + import android.annotation.NonNull; import android.content.Context; import android.content.pm.ActivityInfo; import android.graphics.PixelFormat; import android.graphics.PointF; +import android.os.Bundle; import android.provider.Settings; import android.util.MathUtils; import android.view.Gravity; @@ -28,6 +31,8 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.ImageView; import com.android.internal.annotations.VisibleForTesting; @@ -41,9 +46,15 @@ import com.android.systemui.R; */ class MagnificationModeSwitch { - private static final int DURATION_MS = 5000; - private static final int START_DELAY_MS = 3000; - private final Runnable mAnimationTask; + @VisibleForTesting + static final long FADING_ANIMATION_DURATION_MS = 300; + private static final int DEFAULT_FADE_OUT_ANIMATION_DELAY_MS = 3000; + // The button visible duration starting from the last showButton() called. + private int mVisibleDuration = DEFAULT_FADE_OUT_ANIMATION_DELAY_MS; + private final Runnable mFadeInAnimationTask; + private final Runnable mFadeOutAnimationTask; + @VisibleForTesting + boolean mIsFadeOutAnimating = false; private final Context mContext; private final WindowManager mWindowManager; @@ -71,16 +82,53 @@ class MagnificationModeSwitch { applyResourcesValues(); mImageView.setImageResource(getIconResId(mMagnificationMode)); mImageView.setOnTouchListener(this::onTouch); + mImageView.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setStateDescription(formatStateDescription()); + info.setContentDescription(mContext.getResources().getString( + R.string.magnification_mode_switch_description)); + final AccessibilityAction clickAction = new AccessibilityAction( + AccessibilityAction.ACTION_CLICK.getId(), mContext.getResources().getString( + R.string.magnification_mode_switch_click_label)); + info.addAction(clickAction); + info.setClickable(true); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == AccessibilityAction.ACTION_CLICK.getId()) { + handleSingleTap(); + return true; + } + return super.performAccessibilityAction(host, action, args); + } + }); - mAnimationTask = () -> { + mFadeInAnimationTask = () -> { + mImageView.animate() + .alpha(1f) + .setDuration(FADING_ANIMATION_DURATION_MS) + .start(); + }; + mFadeOutAnimationTask = () -> { mImageView.animate() .alpha(0f) - .setDuration(DURATION_MS) + .setDuration(FADING_ANIMATION_DURATION_MS) .withEndAction(() -> removeButton()) .start(); + mIsFadeOutAnimating = true; }; } + private CharSequence formatStateDescription() { + final int stringId = mMagnificationMode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW + ? R.string.magnification_mode_switch_state_window + : R.string.magnification_mode_switch_state_full_screen; + return mContext.getResources().getString(stringId); + } + private void applyResourcesValues() { final int padding = mContext.getResources().getDimensionPixelSize( R.dimen.magnification_switch_button_padding); @@ -93,7 +141,6 @@ class MagnificationModeSwitch { } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - mImageView.setAlpha(1.0f); mImageView.animate().cancel(); mLastDown.set(event.getRawX(), event.getRawY()); mLastDrag.set(event.getRawX(), event.getRawY()); @@ -134,9 +181,13 @@ class MagnificationModeSwitch { if (!mIsVisible) { return; } + // Reset button status. + mImageView.removeCallbacks(mFadeInAnimationTask); + mImageView.removeCallbacks(mFadeOutAnimationTask); mImageView.animate().cancel(); + mIsFadeOutAnimating = false; + mImageView.setAlpha(0f); mWindowManager.removeView(mImageView); - // Reset button status. mIsVisible = false; mParams.x = 0; mParams.y = 0; @@ -150,14 +201,15 @@ class MagnificationModeSwitch { if (!mIsVisible) { mWindowManager.addView(mImageView, mParams); mIsVisible = true; + mImageView.postOnAnimation(mFadeInAnimationTask); } - mImageView.setAlpha(1.0f); - // TODO(b/143852371): use accessibility timeout as a delay. - // Dismiss the magnification switch button after the button is displayed for a period of - // time. - mImageView.animate().cancel(); - mImageView.removeCallbacks(mAnimationTask); - mImageView.postDelayed(mAnimationTask, START_DELAY_MS); + if (mIsFadeOutAnimating) { + mImageView.animate().cancel(); + mImageView.setAlpha(1f); + } + // Refresh the time slot of the fade-out task whenever this method is called. + mImageView.removeCallbacks(mFadeOutAnimationTask); + mImageView.postOnAnimationDelayed(mFadeOutAnimationTask, mVisibleDuration); } void onConfigurationChanged(int configDiff) { @@ -187,6 +239,7 @@ class MagnificationModeSwitch { imageView.setClickable(true); imageView.setFocusable(true); imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + imageView.setAlpha(0f); return imageView; } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java index 911bf9ef757b..a705ec784c9a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -37,6 +37,7 @@ import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.SystemUI; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.CommandQueue; import javax.inject.Inject; @@ -66,7 +67,8 @@ public class WindowMagnification extends SystemUI implements WindowMagnifierCall @Inject public WindowMagnification(Context context, @Main Handler mainHandler, - CommandQueue commandQueue, ModeSwitchesController modeSwitchesController) { + CommandQueue commandQueue, ModeSwitchesController modeSwitchesController, + NavigationModeController navigationModeController) { super(context); mHandler = mainHandler; mLastConfiguration = new Configuration(context.getResources().getConfiguration()); @@ -77,6 +79,9 @@ public class WindowMagnification extends SystemUI implements WindowMagnifierCall final WindowMagnificationController controller = new WindowMagnificationController(mContext, mHandler, new SfVsyncFrameCallbackProvider(), null, new SurfaceControl.Transaction(), this); + final int navBarMode = navigationModeController.addListener( + controller::onNavigationModeChanged); + controller.onNavigationModeChanged(navBarMode); mWindowMagnificationAnimationController = new WindowMagnificationAnimationController( mContext, controller); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index 714095631fdb..c3474bb7ca57 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -17,6 +17,8 @@ package com.android.systemui.accessibility; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,9 +32,11 @@ import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; +import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.util.Log; +import android.util.Range; import android.view.Choreographer; import android.view.Display; import android.view.Gravity; @@ -47,12 +51,17 @@ import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.R; import com.android.systemui.shared.system.WindowManagerWrapper; +import java.text.NumberFormat; +import java.util.Locale; + /** * Class to handle adding and removing a window magnification. */ @@ -60,6 +69,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold MirrorWindowControl.MirrorWindowDelegate { private static final String TAG = "WindowMagnificationController"; + // Delay to avoid updating state description too frequently. + private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100; + // It should be consistent with the value defined in WindowMagnificationGestureHandler. + private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(2.0f, 8.0f); + private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f; private final Context mContext; private final Resources mResources; private final Handler mHandler; @@ -95,6 +109,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private final View.OnLayoutChangeListener mMirrorViewLayoutChangeListener; private final View.OnLayoutChangeListener mMirrorSurfaceViewLayoutChangeListener; private final Runnable mMirrorViewRunnable; + private final Runnable mUpdateStateDescriptionRunnable; private View mMirrorView; private SurfaceView mMirrorSurfaceView; private int mMirrorSurfaceMargin; @@ -104,8 +119,13 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold // The boundary of magnification frame. private final Rect mMagnificationFrameBoundary = new Rect(); + private int mNavBarMode; + private int mNavGestureHeight; + private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; private Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback; + private Locale mLocale; + private NumberFormat mPercentFormat; @Nullable private MirrorWindowControl mMirrorWindowControl; @@ -164,6 +184,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds); } }; + mUpdateStateDescriptionRunnable = () -> { + if (isWindowVisible()) { + mMirrorView.setStateDescription(formatStateDescription(mScale)); + } + }; } private void updateDimensions() { @@ -175,6 +200,19 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold R.dimen.magnification_drag_view_size); mOuterBorderSize = mResources.getDimensionPixelSize( R.dimen.magnification_outer_border_margin); + updateNavigationBarDimensions(); + } + + private void updateNavigationBarDimensions() { + if (!supportsSwipeUpGesture()) { + mNavGestureHeight = 0; + return; + } + mNavGestureHeight = (mDisplaySize.x > mDisplaySize.y) + ? mResources.getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_height_landscape) + : mResources.getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_gesture_height); } /** @@ -219,6 +257,13 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } } + /** Handles MirrorWindow position when the navigation bar mode changed. */ + public void onNavigationModeChanged(int mode) { + mNavBarMode = mode; + updateNavigationBarDimensions(); + updateMirrorViewLayout(); + } + /** Handles MirrorWindow position when the device rotation changed. */ private void onRotate() { final Display display = mContext.getDisplay(); @@ -226,6 +271,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold display.getRealSize(mDisplaySize); setMagnificationFrameBoundary(); mRotation = display.getRotation(); + updateNavigationBarDimensions(); if (!isWindowVisible()) { return; @@ -292,12 +338,13 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener); + mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate()); + mWm.addView(mMirrorView, params); SurfaceHolder holder = mMirrorSurfaceView.getHolder(); holder.addCallback(this); holder.setFormat(PixelFormat.RGBA_8888); - addDragTouchListeners(); } @@ -380,15 +427,23 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold * moved close to the screen edges. */ private void updateMirrorViewLayout() { + if (!isWindowVisible()) { + return; + } + final int maxMirrorViewX = mDisplaySize.x - mMirrorView.getWidth(); + final int maxMirrorViewY = mDisplaySize.y - mMirrorView.getHeight() - mNavGestureHeight; WindowManager.LayoutParams params = (WindowManager.LayoutParams) mMirrorView.getLayoutParams(); params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; + // If nav bar mode supports swipe-up gesture, the Y position of mirror view should not + // overlap nav bar window to prevent window-dragging obscured. + if (supportsSwipeUpGesture()) { + params.y = Math.min(params.y, maxMirrorViewY); + } // Translates MirrorView position to make MirrorSurfaceView that is inside MirrorView // able to move close to the screen edges. - final int maxMirrorViewX = mDisplaySize.x - mMirrorView.getWidth(); - final int maxMirrorViewY = mDisplaySize.y - mMirrorView.getHeight(); final float translationX; final float translationY; if (params.x < 0) { @@ -526,6 +581,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold final float offsetY = Float.isNaN(centerY) ? 0 : centerY - mMagnificationFrame.exactCenterY(); mScale = Float.isNaN(scale) ? mScale : scale; + setMagnificationFrameBoundary(); updateMagnificationFramePosition((int) offsetX, (int) offsetY); if (!isWindowVisible()) { @@ -546,6 +602,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return; } enableWindowMagnification(scale, Float.NaN, Float.NaN); + mHandler.removeCallbacks(mUpdateStateDescriptionRunnable); + mHandler.postDelayed(mUpdateStateDescriptionRunnable, UPDATE_STATE_DESCRIPTION_DELAY_MS); } /** @@ -596,4 +654,82 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private boolean isWindowVisible() { return mMirrorView != null; } + + private boolean supportsSwipeUpGesture() { + return mNavBarMode == NAV_BAR_MODE_2BUTTON || mNavBarMode == NAV_BAR_MODE_GESTURAL; + } + + private CharSequence formatStateDescription(float scale) { + // Cache the locale-appropriate NumberFormat. Configuration locale is guaranteed + // non-null, so the first time this is called we will always get the appropriate + // NumberFormat, then never regenerate it unless the locale changes on the fly. + final Locale curLocale = mContext.getResources().getConfiguration().getLocales().get(0); + if (!curLocale.equals(mLocale)) { + mLocale = curLocale; + mPercentFormat = NumberFormat.getPercentInstance(curLocale); + } + return mPercentFormat.format(scale); + } + + private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate { + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.addAction( + new AccessibilityAction(R.id.accessibility_action_zoom_in, + mContext.getString(R.string.accessibility_control_zoom_in))); + info.addAction(new AccessibilityAction(R.id.accessibility_action_zoom_out, + mContext.getString(R.string.accessibility_control_zoom_out))); + info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up, + mContext.getString(R.string.accessibility_control_move_up))); + info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down, + mContext.getString(R.string.accessibility_control_move_down))); + info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left, + mContext.getString(R.string.accessibility_control_move_left))); + info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right, + mContext.getString(R.string.accessibility_control_move_right))); + + info.setContentDescription(mContext.getString(R.string.magnification_window_title)); + info.setStateDescription(formatStateDescription(getScale())); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (performA11yAction(action)) { + return true; + } + return super.performAccessibilityAction(host, action, args); + } + + private boolean performA11yAction(int action) { + if (action == R.id.accessibility_action_zoom_in) { + final float scale = mScale + A11Y_CHANGE_SCALE_DIFFERENCE; + setScale(A11Y_ACTION_SCALE_RANGE.clamp(scale)); + return true; + } + if (action == R.id.accessibility_action_zoom_out) { + final float scale = mScale - A11Y_CHANGE_SCALE_DIFFERENCE; + setScale(A11Y_ACTION_SCALE_RANGE.clamp(scale)); + return true; + } + if (action == R.id.accessibility_action_move_up) { + move(0, -mSourceBounds.height()); + return true; + } + if (action == R.id.accessibility_action_move_down) { + move(0, mSourceBounds.height()); + return true; + } + if (action == R.id.accessibility_action_move_left) { + move(-mSourceBounds.width(), 0); + return true; + } + if (action == R.id.accessibility_action_move_right) { + move(mSourceBounds.width(), 0); + return true; + } + return false; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java index 1b2e4c6a595e..c1c2de166627 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java @@ -26,6 +26,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ResolveInfo; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Handler; import android.provider.Settings; @@ -45,6 +47,7 @@ import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; import java.io.PrintWriter; @@ -66,8 +69,10 @@ import dagger.Lazy; @SysUISingleton final class AssistHandleReminderExpBehavior implements BehaviorController { - private static final String LEARNING_TIME_ELAPSED_KEY = "reminder_exp_learning_time_elapsed"; - private static final String LEARNING_EVENT_COUNT_KEY = "reminder_exp_learning_event_count"; + private static final Uri LEARNING_TIME_ELAPSED_URI = + Settings.Secure.getUriFor(Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS); + private static final Uri LEARNING_EVENT_COUNT_URI = + Settings.Secure.getUriFor(Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT); private static final String LEARNED_HINT_LAST_SHOWN_KEY = "reminder_exp_learned_hint_last_shown"; private static final long DEFAULT_LEARNING_TIME_MS = TimeUnit.DAYS.toMillis(10); @@ -167,6 +172,7 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { private final DeviceConfigHelper mDeviceConfigHelper; private final Lazy<StatusBarStateController> mStatusBarStateController; private final Lazy<ActivityManagerWrapper> mActivityManagerWrapper; + private final Lazy<TaskStackChangeListeners> mTaskStackChangeListeners; private final Lazy<OverviewProxyService> mOverviewProxyService; private final Lazy<SysUiState> mSysUiFlagContainer; private final Lazy<WakefulnessLifecycle> mWakefulnessLifecycle; @@ -181,6 +187,7 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { private boolean mIsNavBarHidden; private boolean mIsLauncherShowing; private int mConsecutiveTaskSwitches; + @Nullable private ContentObserver mSettingObserver; /** Whether user has learned the gesture. */ private boolean mIsLearned; @@ -202,6 +209,7 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { DeviceConfigHelper deviceConfigHelper, Lazy<StatusBarStateController> statusBarStateController, Lazy<ActivityManagerWrapper> activityManagerWrapper, + Lazy<TaskStackChangeListeners> taskStackChangeListeners, Lazy<OverviewProxyService> overviewProxyService, Lazy<SysUiState> sysUiFlagContainer, Lazy<WakefulnessLifecycle> wakefulnessLifecycle, @@ -213,6 +221,7 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { mDeviceConfigHelper = deviceConfigHelper; mStatusBarStateController = statusBarStateController; mActivityManagerWrapper = activityManagerWrapper; + mTaskStackChangeListeners = taskStackChangeListeners; mOverviewProxyService = overviewProxyService; mSysUiFlagContainer = sysUiFlagContainer; mWakefulnessLifecycle = wakefulnessLifecycle; @@ -240,7 +249,7 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { ActivityManager.RunningTaskInfo runningTaskInfo = mActivityManagerWrapper.get().getRunningTask(); mRunningTaskId = runningTaskInfo == null ? 0 : runningTaskInfo.taskId; - mActivityManagerWrapper.get().registerTaskStackListener(mTaskStackChangeListener); + mTaskStackChangeListeners.get().registerTaskStackListener(mTaskStackChangeListener); mOverviewProxyService.get().addCallback(mOverviewProxyListener); mSysUiFlagContainer.get().addCallback(mSysUiStateCallback); mIsAwake = mWakefulnessLifecycle.get().getWakefulness() @@ -248,9 +257,22 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { mWakefulnessLifecycle.get().addObserver(mWakefulnessLifecycleObserver); mLearningTimeElapsed = Settings.Secure.getLong( - context.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, /* default = */ 0); + context.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, + /* default = */ 0); mLearningCount = Settings.Secure.getInt( - context.getContentResolver(), LEARNING_EVENT_COUNT_KEY, /* default = */ 0); + context.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, + /* default = */ 0); + mSettingObserver = new SettingsObserver(context, mHandler); + context.getContentResolver().registerContentObserver( + LEARNING_TIME_ELAPSED_URI, + /* notifyForDescendants = */ true, + mSettingObserver); + context.getContentResolver().registerContentObserver( + LEARNING_EVENT_COUNT_URI, + /* notifyForDescendants = */ true, + mSettingObserver); mLearnedHintLastShownEpochDay = Settings.Secure.getLong( context.getContentResolver(), LEARNED_HINT_LAST_SHOWN_KEY, /* default = */ 0); mLastLearningTimestamp = mClock.currentTimeMillis(); @@ -264,13 +286,25 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { if (mContext != null) { mBroadcastDispatcher.get().unregisterReceiver(mDefaultHomeBroadcastReceiver); mBootCompleteCache.get().removeListener(mBootCompleteListener); - Settings.Secure.putLong(mContext.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, 0); - Settings.Secure.putInt(mContext.getContentResolver(), LEARNING_EVENT_COUNT_KEY, 0); + mContext.getContentResolver().unregisterContentObserver(mSettingObserver); + mSettingObserver = null; + // putString in order to use overrideableByRestore + Settings.Secure.putString( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, + Long.toString(0L), + /* overrideableByRestore = */ true); + // putString in order to use overrideableByRestore + Settings.Secure.putString( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, + Integer.toString(0), + /* overrideableByRestore = */ true); Settings.Secure.putLong(mContext.getContentResolver(), LEARNED_HINT_LAST_SHOWN_KEY, 0); mContext = null; } mStatusBarStateController.get().removeCallback(mStatusBarStateListener); - mActivityManagerWrapper.get().unregisterTaskStackListener(mTaskStackChangeListener); + mTaskStackChangeListeners.get().unregisterTaskStackListener(mTaskStackChangeListener); mOverviewProxyService.get().removeCallback(mOverviewProxyListener); mSysUiFlagContainer.get().removeCallback(mSysUiStateCallback); mWakefulnessLifecycle.get().removeObserver(mWakefulnessLifecycleObserver); @@ -282,8 +316,12 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { return; } - Settings.Secure.putLong( - mContext.getContentResolver(), LEARNING_EVENT_COUNT_KEY, ++mLearningCount); + // putString in order to use overrideableByRestore + Settings.Secure.putString( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, + Integer.toString(++mLearningCount), + /* overrideableByRestore = */ true); } @Override @@ -460,8 +498,12 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { mIsLearned = mLearningCount >= getLearningCount() || mLearningTimeElapsed >= getLearningTimeMs(); - mHandler.post(() -> Settings.Secure.putLong( - mContext.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, mLearningTimeElapsed)); + // putString in order to use overrideableByRestore + mHandler.post(() -> Settings.Secure.putString( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, + Long.toString(mLearningTimeElapsed), + /* overrideableByRestore = */ true)); } private void resetConsecutiveTaskSwitches() { @@ -589,4 +631,32 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { + "=" + getShowWhenTaught()); } + + private final class SettingsObserver extends ContentObserver { + + private final Context mContext; + + SettingsObserver(Context context, Handler handler) { + super(handler); + mContext = context; + } + + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + if (LEARNING_TIME_ELAPSED_URI.equals(uri)) { + mLastLearningTimestamp = mClock.currentTimeMillis(); + mLearningTimeElapsed = Settings.Secure.getLong( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, + /* default = */ 0); + } else if (LEARNING_EVENT_COUNT_URI.equals(uri)) { + mLearningCount = Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, + /* default = */ 0); + } + + super.onChange(selfChange, uri); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/assist/PhoneStateMonitor.java b/packages/SystemUI/src/com/android/systemui/assist/PhoneStateMonitor.java index 50d559b7aeab..61951cc0b5e9 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/PhoneStateMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/assist/PhoneStateMonitor.java @@ -35,6 +35,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.StatusBar; @@ -82,7 +83,6 @@ public final class PhoneStateMonitor { mStatusBarOptionalLazy = statusBarOptionalLazy; mStatusBarStateController = Dependency.get(StatusBarStateController.class); - ActivityManagerWrapper activityManagerWrapper = ActivityManagerWrapper.getInstance(); mDefaultHome = getCurrentDefaultHome(); bootCompleteCache.addListener(() -> mDefaultHome = getCurrentDefaultHome()); IntentFilter intentFilter = new IntentFilter(); @@ -95,12 +95,13 @@ public final class PhoneStateMonitor { mDefaultHome = getCurrentDefaultHome(); } }, intentFilter); - mLauncherShowing = isLauncherShowing(activityManagerWrapper.getRunningTask()); - activityManagerWrapper.registerTaskStackListener(new TaskStackChangeListener() { - @Override - public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { - mLauncherShowing = isLauncherShowing(taskInfo); - } + mLauncherShowing = isLauncherShowing(ActivityManagerWrapper.getInstance().getRunningTask()); + TaskStackChangeListeners.getInstance().registerTaskStackListener( + new TaskStackChangeListener() { + @Override + public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { + mLauncherShowing = isLauncherShowing(taskInfo); + } }); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index bde9a6e7c714..b1ae56a584d2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -36,7 +36,7 @@ import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; -import android.hardware.fingerprint.FingerprintSensorProperties; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -306,10 +306,10 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected()) { - final List<FingerprintSensorProperties> fingerprintSensorProperties = - mFingerprintManager.getSensorProperties(); - for (FingerprintSensorProperties props : fingerprintSensorProperties) { - if (props.sensorType == FingerprintSensorProperties.TYPE_UDFPS) { + final List<FingerprintSensorPropertiesInternal> fingerprintSensorProperties = + mFingerprintManager.getSensorPropertiesInternal(); + for (FingerprintSensorPropertiesInternal props : fingerprintSensorProperties) { + if (props.isAnyUdfpsType()) { mUdfpsController = mUdfpsControllerFactory.get(); break; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index d79c96ea4774..e3b00495f3dc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -16,6 +16,7 @@ package com.android.systemui.biometrics; +import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.SuppressLint; @@ -25,6 +26,7 @@ import android.content.res.TypedArray; import android.graphics.PixelFormat; import android.graphics.Point; import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.PowerManager; import android.os.UserHandle; @@ -41,6 +43,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.BrightnessSynchronizer; +import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; @@ -56,7 +59,15 @@ import javax.inject.Inject; /** * Shows and hides the under-display fingerprint sensor (UDFPS) overlay, handles UDFPS touch events, * and coordinates triggering of the high-brightness mode (HBM). + * + * Note that the current architecture is designed so that a single {@link UdfpsController} + * controls/manages all UDFPS sensors. In other words, a single controller is registered with + * {@link com.android.server.biometrics.sensors.fingerprint.FingerprintService}, and interfaces such + * as {@link FingerprintManager#onPointerDown(int, int, int, float, float)} or + * {@link IUdfpsOverlayController#showUdfpsOverlay(int)}should all have + * {@code sensorId} parameters. */ +@SuppressWarnings("deprecation") class UdfpsController implements DozeReceiver { private static final String TAG = "UdfpsController"; // Gamma approximation for the sRGB color space. @@ -64,6 +75,10 @@ class UdfpsController implements DozeReceiver { private static final long AOD_INTERRUPT_TIMEOUT_MILLIS = 1000; private final FingerprintManager mFingerprintManager; + // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple + // sensors, this, in addition to a lot of the code here, will be updated. + @VisibleForTesting + final int mUdfpsSensorId; private final WindowManager mWindowManager; private final SystemSettings mSystemSettings; private final DelayableExecutor mFgExecutor; @@ -103,17 +118,17 @@ class UdfpsController implements DozeReceiver { public class UdfpsOverlayController extends IUdfpsOverlayController.Stub { @Override - public void showUdfpsOverlay() { + public void showUdfpsOverlay(int sensorId) { UdfpsController.this.setShowOverlay(true); } @Override - public void hideUdfpsOverlay() { + public void hideUdfpsOverlay(int sensorId) { UdfpsController.this.setShowOverlay(false); } @Override - public void setDebugMessage(String message) { + public void setDebugMessage(int sensorId, String message) { mView.setDebugMessage(message); } } @@ -165,6 +180,18 @@ class UdfpsController implements DozeReceiver { mFgExecutor = fgExecutor; mLayoutParams = createLayoutParams(context); + int udfpsSensorId = -1; + for (FingerprintSensorPropertiesInternal props : + mFingerprintManager.getSensorPropertiesInternal()) { + if (props.isAnyUdfpsType()) { + udfpsSensorId = props.sensorId; + break; + } + } + // At least one UDFPS sensor exists + checkArgument(udfpsSensorId != -1); + mUdfpsSensorId = udfpsSensorId; + mView = (UdfpsView) inflater.inflate(R.layout.udfps_view, null, false); mHbmPath = resources.getString(R.string.udfps_hbm_sysfs_path); @@ -347,7 +374,7 @@ class UdfpsController implements DozeReceiver { fw.write(mHbmEnableCommand); fw.close(); } - mFingerprintManager.onFingerDown(x, y, minor, major); + mFingerprintManager.onPointerDown(mUdfpsSensorId, x, y, minor, major); } catch (IOException e) { mView.hideScrimAndDot(); Log.e(TAG, "onFingerDown | failed to enable HBM: " + e.getMessage()); @@ -355,7 +382,7 @@ class UdfpsController implements DozeReceiver { } private void onFingerUp() { - mFingerprintManager.onFingerUp(); + mFingerprintManager.onPointerUp(mUdfpsSensorId); // Hiding the scrim before disabling HBM results in less noticeable flicker. mView.hideScrimAndDot(); if (mHbmSupported) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index fbb47e2086b3..57d8dc70cd88 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -15,8 +15,8 @@ */ package com.android.systemui.bubbles; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.os.AsyncTask.Status.FINISHED; -import static android.view.Display.INVALID_DISPLAY; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; import android.app.PendingIntent; +import android.app.Person; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -35,16 +36,18 @@ import android.graphics.Bitmap; import android.graphics.Path; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.os.Parcelable; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.List; import java.util.Objects; /** @@ -169,11 +172,10 @@ class Bubble implements BubbleViewProvider { } @VisibleForTesting(visibility = PRIVATE) - Bubble(@NonNull final NotificationEntry e, + Bubble(@NonNull final BubbleEntry entry, @Nullable final BubbleController.NotificationSuppressionChangedListener listener, final BubbleController.PendingIntentCanceledListener intentCancelListener) { - Objects.requireNonNull(e); - mKey = e.getKey(); + mKey = entry.getKey(); mSuppressionListener = listener; mIntentCancelListener = intent -> { if (mIntent != null) { @@ -181,7 +183,7 @@ class Bubble implements BubbleViewProvider { } intentCancelListener.onPendingIntentCanceled(this); }; - setEntry(e); + setEntry(entry); } @Override @@ -254,7 +256,8 @@ class Bubble implements BubbleViewProvider { } /** - * Cleanup expanded view for bubbles going into overflow. + * Call this to clean up the task for the bubble. Ensure this is always called when done with + * the bubble. */ void cleanupExpandedView() { if (mExpandedView != null) { @@ -268,8 +271,7 @@ class Bubble implements BubbleViewProvider { } /** - * Call when the views should be removed, ensure this is called to clean up ActivityView - * content. + * Call when all the views should be removed/cleaned up. */ void cleanupViews() { cleanupExpandedView(); @@ -294,6 +296,15 @@ class Bubble implements BubbleViewProvider { } /** + * Sets whether this bubble is considered visually interruptive. This method is purely for + * testing. + */ + @VisibleForTesting + void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) { + mIsVisuallyInterruptive = visuallyInterruptive; + } + + /** * Starts a task to inflate & load any necessary information to display a bubble. * * @param callback the callback to notify one the bubble is ready to be displayed. @@ -379,30 +390,28 @@ class Bubble implements BubbleViewProvider { /** * Sets the entry associated with this bubble. */ - void setEntry(@NonNull final NotificationEntry entry) { + void setEntry(@NonNull final BubbleEntry entry) { Objects.requireNonNull(entry); - Objects.requireNonNull(entry.getSbn()); - mLastUpdated = entry.getSbn().getPostTime(); - mIsBubble = entry.getSbn().getNotification().isBubbleNotification(); - mPackageName = entry.getSbn().getPackageName(); - mUser = entry.getSbn().getUser(); + mLastUpdated = entry.getStatusBarNotification().getPostTime(); + mIsBubble = entry.getStatusBarNotification().getNotification().isBubbleNotification(); + mPackageName = entry.getStatusBarNotification().getPackageName(); + mUser = entry.getStatusBarNotification().getUser(); mTitle = getTitle(entry); - mIsClearable = entry.isClearable(); - mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); - mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); - mShouldSuppressPeek = entry.shouldSuppressPeek(); - mChannelId = entry.getSbn().getNotification().getChannelId(); - mNotificationId = entry.getSbn().getId(); - mAppUid = entry.getSbn().getUid(); - mInstanceId = entry.getSbn().getInstanceId(); - mFlyoutMessage = BubbleViewInfoTask.extractFlyoutMessage(entry); - mShortcutInfo = (entry.getRanking() != null ? entry.getRanking().getShortcutInfo() : null); - mMetadataShortcutId = (entry.getBubbleMetadata() != null - ? entry.getBubbleMetadata().getShortcutId() : null); + mChannelId = entry.getStatusBarNotification().getNotification().getChannelId(); + mNotificationId = entry.getStatusBarNotification().getId(); + mAppUid = entry.getStatusBarNotification().getUid(); + mInstanceId = entry.getStatusBarNotification().getInstanceId(); + mFlyoutMessage = extractFlyoutMessage(entry); if (entry.getRanking() != null) { + mShortcutInfo = entry.getRanking().getShortcutInfo(); mIsVisuallyInterruptive = entry.getRanking().visuallyInterruptive(); + if (entry.getRanking().getChannel() != null) { + mIsImportantConversation = + entry.getRanking().getChannel().isImportantConversation(); + } } if (entry.getBubbleMetadata() != null) { + mMetadataShortcutId = entry.getBubbleMetadata().getShortcutId(); mFlags = entry.getBubbleMetadata().getFlags(); mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight(); mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); @@ -419,12 +428,16 @@ class Bubble implements BubbleViewProvider { } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { // Was an intent bubble now it's a shortcut bubble... still unregister the listener mIntent.unregisterCancelListener(mIntentCancelListener); + mIntentActive = false; mIntent = null; } mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); } - mIsImportantConversation = - entry.getChannel() != null && entry.getChannel().isImportantConversation(); + + mIsClearable = entry.isClearable(); + mShouldSuppressNotificationDot = entry.shouldSuppressNotificationDot(); + mShouldSuppressNotificationList = entry.shouldSuppressNotificationList(); + mShouldSuppressPeek = entry.shouldSuppressPeek(); } @Nullable @@ -455,14 +468,6 @@ class Bubble implements BubbleViewProvider { return mIntentActive; } - /** - * @return the display id of the virtual display on which bubble contents is drawn. - */ - @Override - public int getDisplayId() { - return mExpandedView != null ? mExpandedView.getVirtualDisplayId() : INVALID_DISPLAY; - } - public InstanceId getInstanceId() { return mInstanceId; } @@ -477,6 +482,14 @@ class Bubble implements BubbleViewProvider { } /** + * @return the task id of the task in which bubble contents is drawn. + */ + @Override + public int getTaskId() { + return mExpandedView != null ? mExpandedView.getTaskId() : INVALID_TASK_ID; + } + + /** * Should be invoked whenever a Bubble is accessed (selected while expanded). */ void markAsAccessedAt(long lastAccessedMillis) { @@ -725,9 +738,75 @@ class Bubble implements BubbleViewProvider { } @Nullable - private static String getTitle(@NonNull final NotificationEntry e) { - final CharSequence titleCharSeq = e.getSbn().getNotification().extras.getCharSequence( - Notification.EXTRA_TITLE); + private static String getTitle(@NonNull final BubbleEntry e) { + final CharSequence titleCharSeq = e.getStatusBarNotification() + .getNotification().extras.getCharSequence(Notification.EXTRA_TITLE); return titleCharSeq == null ? null : titleCharSeq.toString(); } + + /** + * Returns our best guess for the most relevant text summary of the latest update to this + * notification, based on its type. Returns null if there should not be an update message. + */ + @NonNull + static Bubble.FlyoutMessage extractFlyoutMessage(BubbleEntry entry) { + Objects.requireNonNull(entry); + final Notification underlyingNotif = entry.getStatusBarNotification().getNotification(); + final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); + + Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage(); + bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean( + Notification.EXTRA_IS_GROUP_CONVERSATION); + try { + if (Notification.BigTextStyle.class.equals(style)) { + // Return the big text, it is big so probably important. If it's not there use the + // normal text. + CharSequence bigText = + underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); + bubbleMessage.message = !TextUtils.isEmpty(bigText) + ? bigText + : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); + return bubbleMessage; + } else if (Notification.MessagingStyle.class.equals(style)) { + final List<Notification.MessagingStyle.Message> messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + (Parcelable[]) underlyingNotif.extras.get( + Notification.EXTRA_MESSAGES)); + + final Notification.MessagingStyle.Message latestMessage = + Notification.MessagingStyle.findLatestIncomingMessage(messages); + if (latestMessage != null) { + bubbleMessage.message = latestMessage.getText(); + Person sender = latestMessage.getSenderPerson(); + bubbleMessage.senderName = sender != null ? sender.getName() : null; + bubbleMessage.senderAvatar = null; + bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null; + return bubbleMessage; + } + } else if (Notification.InboxStyle.class.equals(style)) { + CharSequence[] lines = + underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); + + // Return the last line since it should be the most recent. + if (lines != null && lines.length > 0) { + bubbleMessage.message = lines[lines.length - 1]; + return bubbleMessage; + } + } else if (Notification.MediaStyle.class.equals(style)) { + // Return nothing, media updates aren't typically useful as a text update. + return bubbleMessage; + } else { + // Default to text extra. + bubbleMessage.message = + underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); + return bubbleMessage; + } + } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { + // No use crashing, we'll just return null and the caller will assume there's no update + // message. + e.printStackTrace(); + } + + return bubbleMessage; + } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index b6106025a17a..3f94b00d3c60 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -16,6 +16,7 @@ package com.android.systemui.bubbles; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.Notification.FLAG_BUBBLE; import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; @@ -27,13 +28,10 @@ import static android.service.notification.NotificationListenerService.REASON_CL import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; -import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.Display.INVALID_DISPLAY; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_CONTROLLER; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.systemui.statusbar.StatusBarState.SHADE; @@ -47,6 +45,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityTaskManager; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; @@ -71,7 +70,6 @@ import android.util.ArraySet; import android.util.Log; import android.util.Pair; import android.util.SparseSetArray; -import android.view.Display; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -81,17 +79,18 @@ import androidx.annotation.MainThread; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.Dumpable; import com.android.systemui.bubbles.dagger.BubbleModule; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.PinnedStackListenerForwarder; +import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.TaskStackChangeListener; -import com.android.systemui.shared.system.WindowManagerWrapper; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationRemoveInterceptor; @@ -114,7 +113,10 @@ import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ZenModeController; -import com.android.systemui.util.FloatingContentCoordinator; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.pip.PinnedStackListenerForwarder; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -130,7 +132,8 @@ import java.util.Objects; * * The controller manages addition, removal, and visible state of bubbles on screen. */ -public class BubbleController implements ConfigurationController.ConfigurationListener, Dumpable { +public class BubbleController implements Bubbles, ConfigurationController.ConfigurationListener, + Dumpable { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -168,8 +171,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi private final ShadeController mShadeController; private final FloatingContentCoordinator mFloatingContentCoordinator; private final BubbleDataRepository mDataRepository; - private BubbleLogger mLogger = new BubbleLoggerImpl(); - + private BubbleLogger mLogger; + private final Handler mMainHandler; private BubbleData mBubbleData; private ScrimView mBubbleScrim; @Nullable private BubbleStackView mStackView; @@ -231,11 +234,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi */ private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED; + /** + * Last known font scale, used to detect font size changes in {@link #onConfigChanged}. + */ + private float mFontScale = 0; + /** Last known direction, used to detect layout direction changes @link #onConfigChanged}. */ private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED; private boolean mInflateSynchronously; + private MultiWindowTaskListener mTaskListener; + // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline private final List<NotifCallback> mCallbacks = new ArrayList<>(); @@ -345,7 +355,46 @@ public class BubbleController implements ConfigurationController.ConfigurationLi /** * Injected constructor. See {@link BubbleModule}. */ - public BubbleController(Context context, + public static BubbleController create(Context context, + NotificationShadeWindowController notificationShadeWindowController, + StatusBarStateController statusBarStateController, + ShadeController shadeController, + @Nullable BubbleStackView.SurfaceSynchronizer synchronizer, + ConfigurationController configurationController, + NotificationInterruptStateProvider interruptionStateProvider, + ZenModeController zenModeController, + NotificationLockscreenUserManager notifUserManager, + NotificationGroupManagerLegacy groupManager, + NotificationEntryManager entryManager, + NotifPipeline notifPipeline, + FeatureFlags featureFlags, + DumpManager dumpManager, + FloatingContentCoordinator floatingContentCoordinator, + SysUiState sysUiState, + INotificationManager notificationManager, + @Nullable IStatusBarService statusBarService, + WindowManager windowManager, + WindowManagerShellWrapper windowManagerShellWrapper, + LauncherApps launcherApps, + UiEventLogger uiEventLogger, + @Main Handler mainHandler, + ShellTaskOrganizer organizer) { + BubbleLogger logger = new BubbleLogger(uiEventLogger); + return new BubbleController(context, notificationShadeWindowController, + statusBarStateController, shadeController, new BubbleData(context, logger), + synchronizer, configurationController, interruptionStateProvider, zenModeController, + notifUserManager, groupManager, entryManager, notifPipeline, featureFlags, + dumpManager, floatingContentCoordinator, + new BubbleDataRepository(context, launcherApps), sysUiState, notificationManager, + statusBarService, windowManager, windowManagerShellWrapper, launcherApps, logger, + mainHandler, organizer); + } + + /** + * Testing constructor. + */ + @VisibleForTesting + BubbleController(Context context, NotificationShadeWindowController notificationShadeWindowController, StatusBarStateController statusBarStateController, ShadeController shadeController, @@ -366,7 +415,11 @@ public class BubbleController implements ConfigurationController.ConfigurationLi INotificationManager notificationManager, @Nullable IStatusBarService statusBarService, WindowManager windowManager, - LauncherApps launcherApps) { + WindowManagerShellWrapper windowManagerShellWrapper, + LauncherApps launcherApps, + BubbleLogger bubbleLogger, + Handler mainHandler, + ShellTaskOrganizer organizer) { dumpManager.registerDumpable(TAG, this); mContext = context; mShadeController = shadeController; @@ -376,6 +429,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mFloatingContentCoordinator = floatingContentCoordinator; mDataRepository = dataRepository; mINotificationManager = notificationManager; + mLogger = bubbleLogger; + mMainHandler = mainHandler; mZenModeController.addCallback(new ZenModeController.Callback() { @Override public void onZenChanged(int zen) { @@ -414,7 +469,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (bubble.getBubbleIntent() == null) { return; } - if (bubble.isIntentActive()) { + if (bubble.isIntentActive() + || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { bubble.setPendingIntentCanceled(); return; } @@ -438,10 +494,10 @@ public class BubbleController implements ConfigurationController.ConfigurationLi statusBarStateController.addCallback(mStatusBarStateListener); mTaskStackListener = new BubbleTaskStackListener(); - ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); + TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); try { - WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener()); + windowManagerShellWrapper.addPinnedStackListener(new BubblesImeListener()); } catch (RemoteException e) { e.printStackTrace(); } @@ -473,6 +529,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi }); mBubbleIconFactory = new BubbleIconFactory(context); + mTaskListener = new MultiWindowTaskListener(mMainHandler, organizer); launcherApps.registerCallback(new LauncherApps.Callback() { @Override @@ -519,6 +576,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi /** * See {@link NotifCallback}. */ + @Override public void addNotifCallback(NotifCallback callback) { mCallbacks.add(callback); } @@ -534,6 +592,12 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } + private void onBubbleExpandChanged(boolean shouldExpand) { + mSysUiState + .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) + .commitUpdate(mContext.getDisplayId()); + } + private void setupNEM() { mNotificationEntryManager.addNotificationEntryListener( new NotificationEntryListener() { @@ -700,6 +764,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi * since we want the scrim's appearance and behavior to be identical to that of the notification * shade scrim. */ + @Override public ScrimView getScrimForBubble() { return mBubbleScrim; } @@ -708,6 +773,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi * Called when the status bar has become visible or invisible (either permanently or * temporarily). */ + @Override public void onStatusBarVisibilityChanged(boolean visible) { if (mStackView != null) { // Hide the stack temporarily if the status bar has been made invisible, and the stack @@ -725,17 +791,24 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mInflateSynchronously = inflateSynchronously; } - void setOverflowListener(BubbleData.Listener listener) { + @Override + public void setOverflowListener(BubbleData.Listener listener) { mOverflowListener = listener; } /** * @return Bubbles for updating overflow. */ - List<Bubble> getOverflowBubbles() { + @Override + public List<Bubble> getOverflowBubbles() { return mBubbleData.getOverflowBubbles(); } + @Override + public MultiWindowTaskListener getTaskManager() { + return mTaskListener; + } + /** * BubbleStackView is lazily created by this method the first time a Bubble is added. This * method initializes the stack view and adds it to the StatusBar just above the scrim. @@ -744,8 +817,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (mStackView == null) { mStackView = new BubbleStackView( mContext, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator, - mSysUiState, this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged, - this::hideCurrentInputMethod); + this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged, + this::hideCurrentInputMethod, this::onBubbleExpandChanged); mStackView.setStackStartPosition(mPositionFromRemovedStack); mStackView.addView(mBubbleScrim); if (mExpandListener != null) { @@ -778,9 +851,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, - // Start not focusable - we'll become focusable when expanded so the ActivityView - // can use the IME. WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, PixelFormat.TRANSLUCENT); @@ -796,16 +868,13 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mAddedToWindowManager = true; mWindowManager.addView(mStackView, mWmLayoutParams); } catch (IllegalStateException e) { - // This means the stack has already been added. This shouldn't happen, since we keep - // track of that, but just in case, update the previously added view's layout params. + // This means the stack has already been added. This shouldn't happen... e.printStackTrace(); - updateWmFlags(); } } private void onImeVisibilityChanged(boolean imeVisible) { mImeVisible = imeVisible; - updateWmFlags(); } /** Removes the BubbleStackView from the WindowManager if it's there. */ @@ -832,35 +901,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** - * Updates the BubbleStackView's WindowManager.LayoutParams, and updates the WindowManager with - * the new params if the stack has been added. - */ - private void updateWmFlags() { - if (mStackView == null) { - return; - } - if (isStackExpanded() && !mImeVisible) { - // If we're expanded, and the IME isn't visible, we want to be focusable. This ensures - // that any taps within Bubbles (including on the ActivityView) results in Bubbles - // receiving focus and clearing it from any other windows that might have it. - mWmLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } else { - // If we're collapsed, we don't want to be focusable since tapping on the stack would - // steal focus from apps. We also don't want to be focusable if the IME is visible, - mWmLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } - - if (mAddedToWindowManager) { - try { - mWindowManager.updateViewLayout(mStackView, mWmLayoutParams); - } catch (IllegalArgumentException e) { - // If the stack is somehow not there, ignore the attempt to update it. - e.printStackTrace(); - } - } - } - - /** * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been * added in the meantime. */ @@ -948,6 +988,10 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mBubbleIconFactory = new BubbleIconFactory(mContext); mStackView.onDisplaySizeChanged(); } + if (newConfig.fontScale != mFontScale) { + mFontScale = newConfig.fontScale; + mStackView.updateFlyout(mFontScale); + } if (newConfig.getLayoutDirection() != mLayoutDirection) { mLayoutDirection = newConfig.getLayoutDirection(); mStackView.onLayoutDirectionChanged(mLayoutDirection); @@ -955,20 +999,15 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } - boolean inLandscape() { - return mOrientation == Configuration.ORIENTATION_LANDSCAPE; - } - /** * Set a listener to be notified of bubble expand events. */ + @Override public void setExpandListener(BubbleExpandListener listener) { mExpandListener = ((isExpanding, key) -> { if (listener != null) { listener.onBubbleExpandChanged(isExpanding, key); } - - updateWmFlags(); }); if (mStackView != null) { mStackView.setExpandListener(mExpandListener); @@ -987,29 +1026,17 @@ public class BubbleController implements ConfigurationController.ConfigurationLi return mBubbleData.hasBubbles(); } - /** - * Whether the stack of bubbles is expanded or not. - */ + @Override public boolean isStackExpanded() { return mBubbleData.isExpanded(); } - /** - * Tell the stack of bubbles to collapse. - */ + @Override public void collapseStack() { mBubbleData.setExpanded(false /* expanded */); } - /** - * True if either: - * (1) There is a bubble associated with the provided key and if its notification is hidden - * from the shade. - * (2) There is a group summary associated with the provided key that is hidden from the shade - * because it has been dismissed but still has child bubbles active. - * - * False otherwise. - */ + @Override public boolean isBubbleNotificationSuppressedFromShade(NotificationEntry entry) { String key = entry.getKey(); boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key) @@ -1021,19 +1048,14 @@ public class BubbleController implements ConfigurationController.ConfigurationLi return (isSummary && isSuppressedSummary) || isSuppressedBubble; } - /** - * True if: - * (1) The current notification entry same as selected bubble notification entry and the - * stack is currently expanded. - * - * False otherwise. - */ + @Override public boolean isBubbleExpanded(NotificationEntry entry) { return isStackExpanded() && mBubbleData != null && mBubbleData.getSelectedBubble() != null - && mBubbleData.getSelectedBubble().getKey().equals(entry.getKey()) ? true : false; + && mBubbleData.getSelectedBubble().getKey().equals(entry.getKey()); } - void promoteBubbleFromOverflow(Bubble bubble) { + @Override + public void promoteBubbleFromOverflow(Bubble bubble) { mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK); bubble.setInflateSynchronously(mInflateSynchronously); bubble.setShouldAutoExpand(true); @@ -1041,12 +1063,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi setIsBubble(bubble, true /* isBubble */); } - /** - * Request the stack expand if needed, then select the specified Bubble as current. - * If no bubble exists for this entry, one is created. - * - * @param entry the notification for the bubble to be selected - */ + @Override public void expandStackAndSelectBubble(NotificationEntry entry) { if (mStatusBarStateListener.getCurrentState() == SHADE) { mNotifEntryToExpandOnShadeUnlock = null; @@ -1074,12 +1091,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } - /** - * When a notification is marked Priority, expand the stack if needed, - * then (maybe create and) select the given bubble. - * - * @param entry the notification for the bubble to show - */ + @Override public void onUserChangedImportance(NotificationEntry entry) { try { int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; @@ -1095,16 +1107,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** - * Directs a back gesture at the bubble stack. When opened, the current expanded bubble - * is forwarded a back key down/up pair. - */ - public void performBackPressIfNeeded() { - if (mStackView != null) { - mStackView.performBackPressIfNeeded(); - } - } - - /** * Adds or updates a bubble associated with the provided notification entry. * * @param notif the notification associated with this bubble. @@ -1140,8 +1142,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { notif.setInterruption(); } - Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); - inflateAndAdd(bubble, suppressFlyout, showInShade); + if (!notif.getRanking().visuallyInterruptive() + && (notif.getBubbleMetadata() != null + && !notif.getBubbleMetadata().getAutoExpandBubble()) + && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { + // Update the bubble but don't promote it out of overflow + Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); + b.setEntry(notifToBubbleEntry(notif)); + } else { + Bubble bubble = mBubbleData.getOrCreateBubble( + notifToBubbleEntry(notif), null /* persistedBubble */); + inflateAndAdd(bubble, suppressFlyout, showInShade); + } } void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { @@ -1152,15 +1164,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mContext, mStackView, mBubbleIconFactory, false /* skipInflation */); } - /** - * Called when a user has indicated that an active notification should be shown as a bubble. - * <p> - * This method will collapse the shade, create the bubble without a flyout or dot, and suppress - * the notification from appearing in the shade. - * - * @param entry the notification to change bubble state for. - * @param shouldBubble whether the notification should show as a bubble or not. - */ + @Override public void onUserChangedBubble(@NonNull final NotificationEntry entry, boolean shouldBubble) { NotificationChannel channel = entry.getChannel(); final String appPkg = entry.getSbn().getPackageName(); @@ -1199,13 +1203,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } - /** - * Removes the bubble with the given key. - * <p> - * Must be called from the main thread. - */ @MainThread - void removeBubble(String key, int reason) { + @Override + public void removeBubble(String key, int reason) { if (mBubbleData.hasAnyBubbleWithKey(key)) { mBubbleData.dismissBubbleWithKey(key, reason); } @@ -1237,8 +1237,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mBubbleData.removeSuppressedSummary(groupKey); // Remove any associated bubble children with the summary - final List<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup( - groupKey, mNotificationEntryManager); + final List<Bubble> bubbleChildren = getBubblesInGroup(groupKey); for (int i = 0; i < bubbleChildren.size(); i++) { removeBubble(bubbleChildren.get(i).getKey(), DISMISS_GROUP_CANCELLED); } @@ -1284,6 +1283,25 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } + /** + * Retrieves any bubbles that are part of the notification group represented by the provided + * group key. + */ + private ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey) { + ArrayList<Bubble> bubbleChildren = new ArrayList<>(); + if (groupKey == null) { + return bubbleChildren; + } + for (Bubble bubble : mBubbleData.getActiveBubbles()) { + final NotificationEntry entry = + mNotificationEntryManager.getPendingOrActiveNotif(bubble.getKey()); + if (entry != null && groupKey.equals(entry.getSbn().getGroupKey())) { + bubbleChildren.add(bubble); + } + } + return bubbleChildren; + } + private void setIsBubble(@NonNull final NotificationEntry entry, final boolean isBubble, final boolean autoExpand) { Objects.requireNonNull(entry); @@ -1394,8 +1412,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } if (entry != null) { final String groupKey = entry.getSbn().getGroupKey(); - if (mBubbleData.getBubblesInGroup( - groupKey, mNotificationEntryManager).isEmpty()) { + if (getBubblesInGroup(groupKey).isEmpty()) { // Time to potentially remove the summary for (NotifCallback cb : mCallbacks) { cb.maybeCancelSummary(entry); @@ -1447,16 +1464,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } }; - /** - * We intercept notification entries (including group summaries) dismissed by the user when - * there is an active bubble associated with it. We do this so that developers can still - * cancel it (and hence the bubbles associated with it). However, these intercepted - * notifications should then be hidden from the shade since the user has cancelled them, so we - * {@link Bubble#setSuppressNotification}. For the case of suppressed summaries, we also add - * {@link BubbleData#addSummaryToSuppress}. - * - * @return true if we want to intercept the dismissal of the entry, else false. - */ + @Override public boolean handleDismissalInterception(NotificationEntry entry) { if (entry == null) { return false; @@ -1487,8 +1495,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } String groupKey = entry.getSbn().getGroupKey(); - ArrayList<Bubble> bubbleChildren = mBubbleData.getBubblesInGroup( - groupKey, mNotificationEntryManager); + ArrayList<Bubble> bubbleChildren = getBubblesInGroup(groupKey); boolean isSuppressedSummary = (mBubbleData.isSummarySuppressed(groupKey) && mBubbleData.getSummaryKey(groupKey).equals(entry.getKey())); boolean isSummary = entry.getSbn().getNotification().isGroupSummary(); @@ -1580,21 +1587,20 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** - * The display id of the expanded view, if the stack is expanded and not occluded by the - * status bar, otherwise returns {@link Display#INVALID_DISPLAY}. + * The task id of the expanded view, if the stack is expanded and not occluded by the + * status bar, otherwise returns {@link ActivityTaskManager#INVALID_TASK_ID}. */ - public int getExpandedDisplayId(Context context) { + private int getExpandedTaskId() { if (mStackView == null) { - return INVALID_DISPLAY; + return INVALID_TASK_ID; } - final boolean defaultDisplay = context.getDisplay() != null - && context.getDisplay().getDisplayId() == DEFAULT_DISPLAY; final BubbleViewProvider expandedViewProvider = mStackView.getExpandedBubble(); - if (defaultDisplay && expandedViewProvider != null && isStackExpanded() + if (expandedViewProvider != null && isStackExpanded() + && !mStackView.isExpansionAnimating() && !mNotificationShadeWindowController.getPanelExpanded()) { - return expandedViewProvider.getDisplayId(); + return expandedViewProvider.getTaskId(); } - return INVALID_DISPLAY; + return INVALID_TASK_ID; } @VisibleForTesting @@ -1628,18 +1634,17 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onTaskMovedToFront(RunningTaskInfo taskInfo) { - if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) { - if (!mStackView.isExpansionAnimating()) { - mBubbleData.setExpanded(false); - } + int expandedId = getExpandedTaskId(); + if (expandedId != INVALID_TASK_ID && expandedId != taskInfo.taskId) { + mBubbleData.setExpanded(false); } } @Override - public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, + public void onActivityRestartAttempt(RunningTaskInfo taskInfo, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { for (Bubble b : mBubbleData.getBubbles()) { - if (b.getDisplayId() == task.displayId) { + if (taskInfo.taskId == b.getTaskId()) { mBubbleData.setSelectedBubble(b); mBubbleData.setExpanded(true); return; @@ -1647,43 +1652,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } - @Override - public void onActivityLaunchOnSecondaryDisplayRerouted() { - if (mStackView != null) { - mBubbleData.setExpanded(false); - } - } - - @Override - public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { - if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) { - if (mImeVisible) { - hideCurrentInputMethod(); - } else { - mBubbleData.setExpanded(false); - } - } - } - - @Override - public void onSingleTaskDisplayDrawn(int displayId) { - if (mStackView == null) { - return; - } - mStackView.showExpandedViewContents(displayId); - } - - @Override - public void onSingleTaskDisplayEmpty(int displayId) { - final BubbleViewProvider expandedBubble = mStackView != null - ? mStackView.getExpandedBubble() - : null; - int expandedId = expandedBubble != null ? expandedBubble.getDisplayId() : -1; - if (mStackView != null && mStackView.isExpanded() && expandedId == displayId) { - mBubbleData.setExpanded(false); - } - mBubbleData.notifyDisplayEmpty(displayId); - } } /** @@ -1727,6 +1695,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** PinnedStackListener that dispatches IME visibility updates to the stack. */ + //TODO(b/170442945): Better way to do this / insets listener? private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedStackListener { @Override public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { @@ -1735,4 +1704,10 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } } + + static BubbleEntry notifToBubbleEntry(NotificationEntry e) { + return new BubbleEntry(e.getSbn(), e.getRanking(), e.isClearable(), + e.shouldSuppressNotificationDot(), e.shouldSuppressNotificationList(), + e.shouldSuppressPeek()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index bab18ec053ee..b4626f27d370 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -32,12 +32,9 @@ import android.view.View; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; import com.android.systemui.R; import com.android.systemui.bubbles.BubbleController.DismissReason; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.shared.system.SysUiStatsLog; -import com.android.systemui.statusbar.notification.NotificationEntryManager; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -52,15 +49,12 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; -import javax.inject.Inject; - /** * Keeps track of active bubbles. */ -@SysUISingleton public class BubbleData { - private BubbleLoggerImpl mLogger = new BubbleLoggerImpl(); + private BubbleLogger mLogger; private int mCurrentUserId; @@ -154,14 +148,14 @@ public class BubbleData { * associated with it). This list is used to check if the summary should be hidden from the * shade. * - * Key: group key of the NotificationEntry - * Value: key of the NotificationEntry + * Key: group key of the notification + * Value: key of the notification */ private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>(); - @Inject - public BubbleData(Context context) { + public BubbleData(Context context, BubbleLogger bubbleLogger) { mContext = context; + mLogger = bubbleLogger; mBubbles = new ArrayList<>(); mOverflowBubbles = new ArrayList<>(); mPendingBubbles = new HashMap<>(); @@ -205,6 +199,11 @@ public class BubbleData { return mSelectedBubble; } + /** Return a read-only current active bubble lists. */ + public List<Bubble> getActiveBubbles() { + return Collections.unmodifiableList(mBubbles); + } + public void setExpanded(boolean expanded) { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "setExpanded: " + expanded); @@ -235,8 +234,8 @@ public class BubbleData { * @param persistedBubble The bubble to use, only non-null if it's a bubble being promoted from * the overflow that was persisted over reboot. */ - Bubble getOrCreateBubble(NotificationEntry entry, Bubble persistedBubble) { - String key = entry != null ? entry.getKey() : persistedBubble.getKey(); + public Bubble getOrCreateBubble(BubbleEntry entry, Bubble persistedBubble) { + String key = persistedBubble != null ? persistedBubble.getKey() : entry.getKey(); Bubble bubbleToReturn = getBubbleInStackWithKey(key); if (bubbleToReturn == null) { @@ -266,7 +265,7 @@ public class BubbleData { /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, - * BubbleStackView, BubbleIconFactory). + * BubbleStackView, BubbleIconFactory, boolean). */ void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { if (DEBUG_BUBBLE_DATA) { @@ -284,7 +283,8 @@ public class BubbleData { } else { // Updates an existing bubble bubble.setSuppressFlyout(suppressFlyout); - doUpdate(bubble); + // If there is no flyout, we probably shouldn't show the bubble at the top + doUpdate(bubble, !suppressFlyout /* reorder */); } if (bubble.shouldAutoExpand()) { @@ -328,7 +328,7 @@ public class BubbleData { * Retrieves the notif entry key of the summary associated with the provided group key. * * @param groupKey the group to look up - * @return the key for the {@link NotificationEntry} that is the summary of this group. + * @return the key for the notification that is the summary of this group. */ String getSummaryKey(String groupKey) { return mSuppressedGroupKeys.get(groupKey); @@ -349,25 +349,6 @@ public class BubbleData { } /** - * Retrieves any bubbles that are part of the notification group represented by the provided - * group key. - */ - ArrayList<Bubble> getBubblesInGroup(@Nullable String groupKey, @NonNull - NotificationEntryManager nem) { - ArrayList<Bubble> bubbleChildren = new ArrayList<>(); - if (groupKey == null) { - return bubbleChildren; - } - for (Bubble b : mBubbles) { - final NotificationEntry entry = nem.getPendingOrActiveNotif(b.getKey()); - if (entry != null && groupKey.equals(entry.getSbn().getGroupKey())) { - bubbleChildren.add(b); - } - } - return bubbleChildren; - } - - /** * Removes bubbles from the given package whose shortcut are not in the provided list of valid * shortcuts. */ @@ -438,12 +419,12 @@ public class BubbleData { } } - private void doUpdate(Bubble bubble) { + private void doUpdate(Bubble bubble, boolean reorder) { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "doUpdate: " + bubble); } mStateChange.updatedBubble = bubble; - if (!isExpanded()) { + if (!isExpanded() && reorder) { int prevPos = mBubbles.indexOf(bubble); mBubbles.remove(bubble); mBubbles.add(0, bubble); @@ -570,22 +551,6 @@ public class BubbleData { dispatchPendingChanges(); } - /** - * Indicates that the provided display is no longer in use and should be cleaned up. - * - * @param displayId the id of the display to clean up. - */ - void notifyDisplayEmpty(int displayId) { - for (Bubble b : mBubbles) { - if (b.getDisplayId() == displayId) { - if (b.getExpandedView() != null) { - b.getExpandedView().notifyDisplayEmpty(); - } - return; - } - } - } - private void dispatchPendingChanges() { if (mListener != null && mStateChange.anythingChanged()) { mListener.applyUpdate(mStateChange); @@ -636,12 +601,12 @@ public class BubbleData { * @param normalX Normalized x position of the stack * @param normalY Normalized y position of the stack */ - void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName, + void logBubbleEvent(@Nullable BubbleViewProvider provider, int action, String packageName, int bubbleCount, int bubbleIndex, float normalX, float normalY) { if (provider == null) { mLogger.logStackUiChanged(packageName, action, bubbleCount, normalX, normalY); } else if (provider.getKey().equals(BubbleOverflow.KEY)) { - if (action == SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED) { + if (action == FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED) { mLogger.logShowOverflow(packageName, mCurrentUserId); } } else { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt index f129d3147032..2ab9e8734bef 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.bubbles import android.annotation.SuppressLint import android.annotation.UserIdInt +import android.content.Context import android.content.pm.LauncherApps import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC @@ -26,21 +27,16 @@ import android.util.Log import com.android.systemui.bubbles.storage.BubbleEntity import com.android.systemui.bubbles.storage.BubblePersistentRepository import com.android.systemui.bubbles.storage.BubbleVolatileRepository -import com.android.systemui.dagger.SysUISingleton import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import kotlinx.coroutines.yield -import javax.inject.Inject -@SysUISingleton -internal class BubbleDataRepository @Inject constructor( - private val volatileRepository: BubbleVolatileRepository, - private val persistentRepository: BubblePersistentRepository, - private val launcherApps: LauncherApps -) { +internal class BubbleDataRepository(context: Context, private val launcherApps: LauncherApps) { + private val volatileRepository = BubbleVolatileRepository(launcherApps) + private val persistentRepository = BubblePersistentRepository(context) private val ioScope = CoroutineScope(Dispatchers.IO) private val uiScope = CoroutineScope(Dispatchers.Main) diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleEntry.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleEntry.java new file mode 100644 index 000000000000..6a1302518699 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleEntry.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2020 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.bubbles; + +import android.app.Notification.BubbleMetadata; +import android.app.NotificationManager.Policy; +import android.service.notification.NotificationListenerService.Ranking; +import android.service.notification.StatusBarNotification; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Represents a notification with needed data and flag for bubbles. + * + * @see Bubble + */ +public class BubbleEntry { + + private StatusBarNotification mSbn; + private Ranking mRanking; + + private boolean mIsClearable; + private boolean mShouldSuppressNotificationDot; + private boolean mShouldSuppressNotificationList; + private boolean mShouldSuppressPeek; + + public BubbleEntry(@NonNull StatusBarNotification sbn, + Ranking ranking, boolean isClearable, boolean shouldSuppressNotificationDot, + boolean shouldSuppressNotificationList, boolean shouldSuppressPeek) { + mSbn = sbn; + mRanking = ranking; + + mIsClearable = isClearable; + mShouldSuppressNotificationDot = shouldSuppressNotificationDot; + mShouldSuppressNotificationList = shouldSuppressNotificationList; + mShouldSuppressPeek = shouldSuppressPeek; + } + + /** @return the {@link StatusBarNotification} for this entry. */ + @NonNull + public StatusBarNotification getStatusBarNotification() { + return mSbn; + } + + /** @return the {@link Ranking} for this entry. */ + public Ranking getRanking() { + return mRanking; + } + + /** @return the key in the {@link StatusBarNotification}. */ + public String getKey() { + return mSbn.getKey(); + } + + /** @return the {@link BubbleMetadata} in the {@link StatusBarNotification}. */ + @Nullable + public BubbleMetadata getBubbleMetadata() { + return getStatusBarNotification().getNotification().getBubbleMetadata(); + } + + /** @return true if this notification is clearable. */ + public boolean isClearable() { + return mIsClearable; + } + + /** @return true if {@link Policy#SUPPRESSED_EFFECT_BADGE} set for this notification. */ + public boolean shouldSuppressNotificationDot() { + return mShouldSuppressNotificationDot; + } + + /** + * @return true if {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST} + * set for this notification. + */ + public boolean shouldSuppressNotificationList() { + return mShouldSuppressNotificationList; + } + + /** @return true if {@link Policy#SUPPRESSED_EFFECT_PEEK} set for this notification. */ + public boolean shouldSuppressPeek() { + return mShouldSuppressPeek; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index ec60cbd175d0..98a2257d2daa 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -16,27 +16,19 @@ package com.android.systemui.bubbles; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; -import static android.graphics.PixelFormat.TRANSPARENT; -import static android.view.Display.INVALID_DISPLAY; -import static android.view.InsetsState.ITYPE_IME; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; -import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.systemui.bubbles.BubbleOverflowActivity.EXTRA_BUBBLE_CONTROLLER; import android.annotation.NonNull; import android.annotation.SuppressLint; -import android.app.ActivityManager; import android.app.ActivityOptions; -import android.app.ActivityTaskManager; -import android.app.ActivityView; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -49,18 +41,13 @@ import android.graphics.Outline; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; -import android.hardware.display.VirtualDisplay; -import android.os.Binder; -import android.os.RemoteException; +import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; -import android.view.Gravity; import android.view.SurfaceControl; -import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; -import android.view.WindowInsets; import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; @@ -82,18 +69,6 @@ import java.io.PrintWriter; */ public class BubbleExpandedView extends LinearLayout { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; - private static final String WINDOW_TITLE = "ImeInsetsWindowWithoutContent"; - - private enum ActivityViewStatus { - // ActivityView is being initialized, cannot start an activity yet. - INITIALIZING, - // ActivityView is initialized, and ready to start an activity. - INITIALIZED, - // Activity runs in the ActivityView. - ACTIVITY_STARTED, - // ActivityView is released, so activity launching will no longer be permitted. - RELEASED, - } // The triangle pointing to the expanded view private View mPointerView; @@ -101,16 +76,11 @@ public class BubbleExpandedView extends LinearLayout { @Nullable private int[] mExpandedViewContainerLocation; private AlphaOptimizedButton mSettingsIcon; + private TaskView mTaskView; - // Views for expanded state - private ActivityView mActivityView; - - private ActivityViewStatus mActivityViewStatus = ActivityViewStatus.INITIALIZING; - private int mTaskId = -1; - - private PendingIntent mPendingIntent; + private int mTaskId = INVALID_TASK_ID; - private boolean mKeyboardVisible; + private boolean mImeVisible; private boolean mNeedsNewHeight; private Point mDisplaySize; @@ -121,131 +91,119 @@ public class BubbleExpandedView extends LinearLayout { private int mPointerHeight; private ShapeDrawable mPointerDrawable; private int mExpandedViewPadding; - + private float mCornerRadius = 0f; @Nullable private Bubble mBubble; + private PendingIntent mPendingIntent; private boolean mIsOverflow; - private BubbleController mBubbleController = Dependency.get(BubbleController.class); + private Bubbles mBubbles = Dependency.get(Bubbles.class); private WindowManager mWindowManager; - private ActivityManager mActivityManager; - private BubbleStackView mStackView; - private View mVirtualImeView; - private WindowManager mVirtualDisplayWindowManager; - private boolean mImeShowing = false; - private float mCornerRadius = 0f; /** * Container for the ActivityView that has a solid, round-rect background that shows if the * ActivityView hasn't loaded. */ - private FrameLayout mActivityViewContainer = new FrameLayout(getContext()); + private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext()); - /** The SurfaceView that the ActivityView draws to. */ - @Nullable private SurfaceView mActivitySurface; + private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { + private boolean mInitialized = false; + private boolean mDestroyed = false; - private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() { @Override - public void onActivityViewReady(ActivityView view) { + public void onInitialized() { if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onActivityViewReady: mActivityViewStatus=" + mActivityViewStatus + Log.d(TAG, "onActivityViewReady: destroyed=" + mDestroyed + + " initialized=" + mInitialized + " bubble=" + getBubbleKey()); } - switch (mActivityViewStatus) { - case INITIALIZING: - case INITIALIZED: - // Custom options so there is no activity transition animation - ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), - 0 /* enterResId */, 0 /* exitResId */); - options.setTaskAlwaysOnTop(true); - // Soptions.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - // Post to keep the lifecycle normal - post(() -> { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onActivityViewReady: calling startActivity, " - + "bubble=" + getBubbleKey()); - } - if (mActivityView == null) { - mBubbleController.removeBubble(getBubbleKey(), - BubbleController.DISMISS_INVALID_INTENT); - return; - } - try { - if (!mIsOverflow && mBubble.hasMetadataShortcutId() - && mBubble.getShortcutInfo() != null) { - options.setApplyActivityFlagsForBubbles(true); - mActivityView.startShortcutActivity(mBubble.getShortcutInfo(), - options, null /* sourceBounds */); - } else { - Intent fillInIntent = new Intent(); - // Apply flags to make behaviour match documentLaunchMode=always. - fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - if (mBubble != null) { - mBubble.setIntentActive(); - } - mActivityView.startActivity(mPendingIntent, fillInIntent, options); - } - } catch (RuntimeException e) { - // If there's a runtime exception here then there's something - // wrong with the intent, we can't really recover / try to populate - // the bubble again so we'll just remove it. - Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() - + ", " + e.getMessage() + "; removing bubble"); - mBubbleController.removeBubble(getBubbleKey(), - BubbleController.DISMISS_INVALID_INTENT); - } - }); - mActivityViewStatus = ActivityViewStatus.ACTIVITY_STARTED; - break; - case ACTIVITY_STARTED: - post(() -> mActivityManager.moveTaskToFront(mTaskId, 0)); - break; + + if (mDestroyed || mInitialized) { + return; } + // Custom options so there is no activity transition animation + ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), + 0 /* enterResId */, 0 /* exitResId */); + + // TODO: I notice inconsistencies in lifecycle + // Post to keep the lifecycle normal + post(() -> { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onActivityViewReady: calling startActivity, bubble=" + + getBubbleKey()); + } + try { + if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { + mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), + options, null /* sourceBounds */); + } else { + Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + if (mBubble != null) { + mBubble.setIntentActive(); + } + mTaskView.startActivity(mPendingIntent, fillInIntent, options); + } + } catch (RuntimeException e) { + // If there's a runtime exception here then there's something + // wrong with the intent, we can't really recover / try to populate + // the bubble again so we'll just remove it. + Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() + + ", " + e.getMessage() + "; removing bubble"); + mBubbles.removeBubble(getBubbleKey(), + BubbleController.DISMISS_INVALID_INTENT); + } + }); + mInitialized = true; } @Override - public void onActivityViewDestroyed(ActivityView view) { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onActivityViewDestroyed: mActivityViewStatus=" + mActivityViewStatus - + " bubble=" + getBubbleKey()); - } - mActivityViewStatus = ActivityViewStatus.RELEASED; + public void onReleased() { + mDestroyed = true; } @Override - public void onTaskCreated(int taskId, ComponentName componentName) { + public void onTaskCreated(int taskId, ComponentName name) { if (DEBUG_BUBBLE_EXPANDED_VIEW) { Log.d(TAG, "onTaskCreated: taskId=" + taskId + " bubble=" + getBubbleKey()); } - // Since Bubble ActivityView applies singleTaskDisplay this is - // guaranteed to only be called once per ActivityView. The taskId is - // saved to use for removeTask, preventing appearance in recent tasks. + // The taskId is saved to use for removeTask, preventing appearance in recent tasks. mTaskId = taskId; + + // With the task org, the taskAppeared callback will only happen once the task has + // already drawn + setContentVisibility(true); + } + + @Override + public void onTaskVisibilityChanged(int taskId, boolean visible) { + setContentVisibility(visible); } - /** - * This is only called for tasks on this ActivityView, which is also set to - * single-task mode -- meaning never more than one task on this display. If a task - * is being removed, it's the top Activity finishing and this bubble should - * be removed or collapsed. - */ @Override public void onTaskRemovalStarted(int taskId) { if (DEBUG_BUBBLE_EXPANDED_VIEW) { Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId - + " mActivityViewStatus=" + mActivityViewStatus + " bubble=" + getBubbleKey()); } if (mBubble != null) { // Must post because this is called from a binder thread. - post(() -> mBubbleController.removeBubble(mBubble.getKey(), + post(() -> mBubbles.removeBubble(mBubble.getKey(), BubbleController.DISMISS_TASK_FINISHED)); } } + + @Override + public void onBackPressedOnTaskRoot(int taskId) { + if (mTaskId == taskId && mStackView.isExpanded()) { + mBubbles.collapseStack(); + } + } }; public BubbleExpandedView(Context context) { @@ -264,7 +222,6 @@ public class BubbleExpandedView extends LinearLayout { int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); updateDimensions(); - mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); } void updateDimensions() { @@ -282,9 +239,6 @@ public class BubbleExpandedView extends LinearLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "onFinishInflate: bubble=" + getBubbleKey()); - } Resources res = getResources(); mPointerView = findViewById(R.id.pointer_view); @@ -299,35 +253,21 @@ public class BubbleExpandedView extends LinearLayout { R.dimen.bubble_manage_button_height); mSettingsIcon = findViewById(R.id.settings_button); - mActivityView = new ActivityView.Builder(mContext) - .setSingleInstance(true) - .setDisableSurfaceViewBackgroundLayer(true) - .setUseTrustedDisplay(true) - .build(); - + mTaskView = new TaskView(mContext, mBubbles.getTaskManager()); // Set ActivityView's alpha value as zero, since there is no view content to be shown. setContentVisibility(false); - mActivityViewContainer.setOutlineProvider(new ViewOutlineProvider() { + mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); } }); - mActivityViewContainer.setClipToOutline(true); - mActivityViewContainer.addView(mActivityView); - mActivityViewContainer.setLayoutParams( + mExpandedViewContainer.setClipToOutline(true); + mExpandedViewContainer.addView(mTaskView); + mExpandedViewContainer.setLayoutParams( new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); - addView(mActivityViewContainer); - - if (mActivityView != null - && mActivityView.getChildCount() > 0 - && mActivityView.getChildAt(0) instanceof SurfaceView) { - // Retrieve the surface from the ActivityView so we can screenshot it and change its - // z-ordering. This should always be possible, since ActivityView's constructor adds the - // SurfaceView as its first child. - mActivitySurface = (SurfaceView) mActivityView.getChildAt(0); - } + addView(mExpandedViewContainer); // Expanded stack layout, top to bottom: // Expanded view container @@ -335,33 +275,22 @@ public class BubbleExpandedView extends LinearLayout { // ==> expanded view // ==> activity view // ==> manage button - bringChildToFront(mActivityView); + bringChildToFront(mTaskView); bringChildToFront(mSettingsIcon); + mTaskView.setListener(mTaskViewListener); applyThemeAttrs(); - setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { - // Keep track of IME displaying because we should not make any adjustments that might - // cause a config change while the IME is displayed otherwise it'll loose focus. - final int keyboardHeight = insets.getSystemWindowInsetBottom() - - insets.getStableInsetBottom(); - mKeyboardVisible = keyboardHeight != 0; - if (!mKeyboardVisible && mNeedsNewHeight) { - updateHeight(); - } - return view.onApplyWindowInsets(insets); - }); - mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); setPadding(mExpandedViewPadding, mExpandedViewPadding, mExpandedViewPadding, mExpandedViewPadding); setOnTouchListener((view, motionEvent) -> { - if (!usingActivityView()) { + if (mTaskView == null) { return false; } final Rect avBounds = new Rect(); - mActivityView.getBoundsOnScreen(avBounds); + mTaskView.getBoundsOnScreen(avBounds); // Consume and ignore events on the expanded view padding that are within the // ActivityView's vertical bounds. These events are part of a back gesture, and so they @@ -387,51 +316,58 @@ public class BubbleExpandedView extends LinearLayout { } /** - * Asks the ActivityView's surface to draw on top of all other views in the window. This is - * useful for ordering surfaces during animations, but should otherwise be set to false so that - * bubbles and menus can draw over the ActivityView. + * Sets whether the surface displaying app content should sit on top. This is useful for + * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble + * being dragged out, the manage menu) this is set to false, otherwise it should be true. */ void setSurfaceZOrderedOnTop(boolean onTop) { - if (mActivitySurface == null) { + if (mTaskView == null) { return; } + mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */); + } - mActivitySurface.setZOrderedOnTop(onTop, true); + void setImeVisible(boolean visible) { + mImeVisible = visible; + if (!mImeVisible && mNeedsNewHeight) { + updateHeight(); + } } - /** Return a GraphicBuffer with the contents of the ActivityView's underlying surface. */ + /** Return a GraphicBuffer with the contents of the task view surface. */ @Nullable SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() { - if (mActivitySurface == null) { + if (mTaskView == null) { return null; } - return SurfaceControl.captureLayers( - mActivitySurface.getSurfaceControl(), - new Rect(0, 0, mActivityView.getWidth(), mActivityView.getHeight()), + mTaskView.getSurfaceControl(), + new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()), 1 /* scale */); } - int[] getActivityViewLocationOnScreen() { - if (mActivityView != null) { - return mActivityView.getLocationOnScreen(); + int[] getTaskViewLocationOnScreen() { + if (mTaskView != null) { + return mTaskView.getLocationOnScreen(); } else { return new int[]{0, 0}; } } + // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this void setManageClickListener(OnClickListener manageClickListener) { - findViewById(R.id.settings_button).setOnClickListener(manageClickListener); + mSettingsIcon.setOnClickListener(manageClickListener); } /** - * Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which - * results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful - * if a view has been added or removed from on top of the ActivityView, such as the manage menu. + * Updates the obscured touchable region for the task surface. This calls onLocationChanged, + * which results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is + * useful if a view has been added or removed from on top of the ActivityView, such as the + * manage menu. */ void updateObscuredTouchableRegion() { - if (mActivityView != null) { - mActivityView.onLocationChanged(); + if (mTaskView != null) { + mTaskView.onLocationChanged(); } } @@ -440,12 +376,12 @@ public class BubbleExpandedView extends LinearLayout { android.R.attr.dialogCornerRadius, android.R.attr.colorBackgroundFloating}); mCornerRadius = ta.getDimensionPixelSize(0, 0); - mActivityViewContainer.setBackgroundColor(ta.getColor(1, Color.WHITE)); + mExpandedViewContainer.setBackgroundColor(ta.getColor(1, Color.WHITE)); ta.recycle(); - if (mActivityView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( + if (mTaskView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( mContext.getResources())) { - mActivityView.setCornerRadius(mCornerRadius); + mTaskView.setCornerRadius(mCornerRadius); } final int mode = @@ -464,11 +400,8 @@ public class BubbleExpandedView extends LinearLayout { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mKeyboardVisible = false; + mImeVisible = false; mNeedsNewHeight = false; - if (mActivityView != null) { - setImeWindowToDisplay(0, 0); - } if (DEBUG_BUBBLE_EXPANDED_VIEW) { Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey()); } @@ -490,84 +423,23 @@ public class BubbleExpandedView extends LinearLayout { final float alpha = visibility ? 1f : 0f; mPointerView.setAlpha(alpha); - - if (mActivityView != null && alpha != mActivityView.getAlpha()) { - mActivityView.setAlpha(alpha); - mActivityView.bringToFront(); + if (mTaskView == null) { + return; + } + if (alpha != mTaskView.getAlpha()) { + mTaskView.setAlpha(alpha); } } - @Nullable ActivityView getActivityView() { - return mActivityView; + @Nullable + View getTaskView() { + return mTaskView; } int getTaskId() { return mTaskId; } - /** - * Called by {@link BubbleStackView} when the insets for the expanded state should be updated. - * This should be done post-move and post-animation. - */ - void updateInsets(WindowInsets insets) { - if (usingActivityView()) { - int[] screenLoc = mActivityView.getLocationOnScreen(); - final int activityViewBottom = screenLoc[1] + mActivityView.getHeight(); - final int keyboardTop = mDisplaySize.y - Math.max(insets.getSystemWindowInsetBottom(), - insets.getDisplayCutout() != null - ? insets.getDisplayCutout().getSafeInsetBottom() - : 0); - setImeWindowToDisplay(getWidth(), Math.max(activityViewBottom - keyboardTop, 0)); - } - } - - private void setImeWindowToDisplay(int w, int h) { - if (getVirtualDisplayId() == INVALID_DISPLAY) { - return; - } - if (h == 0 || w == 0) { - if (mImeShowing) { - mVirtualImeView.setVisibility(GONE); - mImeShowing = false; - } - return; - } - final Context virtualDisplayContext = mContext.createDisplayContext( - getVirtualDisplay().getDisplay()); - - if (mVirtualDisplayWindowManager == null) { - mVirtualDisplayWindowManager = - (WindowManager) virtualDisplayContext.getSystemService(Context.WINDOW_SERVICE); - } - if (mVirtualImeView == null) { - mVirtualImeView = new View(virtualDisplayContext); - mVirtualImeView.setVisibility(VISIBLE); - mVirtualDisplayWindowManager.addView(mVirtualImeView, - getVirtualImeViewAttrs(w, h)); - } else { - mVirtualDisplayWindowManager.updateViewLayout(mVirtualImeView, - getVirtualImeViewAttrs(w, h)); - mVirtualImeView.setVisibility(VISIBLE); - } - - mImeShowing = true; - } - - private WindowManager.LayoutParams getVirtualImeViewAttrs(int w, int h) { - // To use TYPE_NAVIGATION_BAR_PANEL instead of TYPE_IME_BAR to bypass the IME window type - // token check when adding the window. - final WindowManager.LayoutParams attrs = - new WindowManager.LayoutParams(w, h, TYPE_NAVIGATION_BAR_PANEL, - FLAG_LAYOUT_NO_LIMITS | FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, - TRANSPARENT); - attrs.gravity = Gravity.BOTTOM; - attrs.setTitle(WINDOW_TITLE); - attrs.token = new Binder(); - attrs.providesInsetsTypes = new int[]{ITYPE_IME}; - attrs.alpha = 0.0f; - return attrs; - } - void setStackView(BubbleStackView stackView) { mStackView = stackView; } @@ -576,7 +448,10 @@ public class BubbleExpandedView extends LinearLayout { mIsOverflow = overflow; Intent target = new Intent(mContext, BubbleOverflowActivity.class); - mPendingIntent = PendingIntent.getActivity(mContext, /* requestCode */ 0, + Bundle extras = new Bundle(); + extras.putBinder(EXTRA_BUBBLE_CONTROLLER, ObjectWrapper.wrap(mBubbles)); + target.putExtras(extras); + mPendingIntent = PendingIntent.getActivity(mContext, 0 /* requestCode */, target, PendingIntent.FLAG_UPDATE_CURRENT); mSettingsIcon.setVisibility(GONE); } @@ -614,7 +489,7 @@ public class BubbleExpandedView extends LinearLayout { mPendingIntent = mBubble.getBubbleIntent(); if (mPendingIntent != null || mBubble.hasMetadataShortcutId()) { setContentVisibility(false); - mActivityView.setVisibility(VISIBLE); + mTaskView.setVisibility(VISIBLE); } } applyThemeAttrs(); @@ -624,59 +499,42 @@ public class BubbleExpandedView extends LinearLayout { } } + /** + * Bubbles are backed by a pending intent or a shortcut, once the activity is + * started we never change it / restart it on notification updates -- unless the bubbles' + * backing data switches. + * + * This indicates if the new bubble is backed by a different data source than what was + * previously shown here (e.g. previously a pending intent & now a shortcut). + * + * @param newBubble the bubble this view is being updated with. + * @return true if the backing content has changed. + */ private boolean didBackingContentChange(Bubble newBubble) { boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; boolean newIsIntentBased = newBubble.getBubbleIntent() != null; return prevWasIntentBased != newIsIntentBased; } - /** - * Lets activity view know it should be shown / populated with activity content. - */ - void populateExpandedView() { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "populateExpandedView: " - + "bubble=" + getBubbleKey()); - } - - if (usingActivityView()) { - mActivityView.setCallback(mStateCallback); - } else { - Log.e(TAG, "Cannot populate expanded view."); - } - } - - boolean performBackPressIfNeeded() { - if (!usingActivityView()) { - return false; - } - mActivityView.performBackPress(); - return true; - } - void updateHeight() { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()); - } - if (mExpandedViewContainerLocation == null) { return; } - if (usingActivityView()) { + if (mBubble != null || mIsOverflow) { float desiredHeight = mOverflowHeight; if (!mIsOverflow) { desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight); } float height = Math.min(desiredHeight, getMaxExpandedHeight()); - height = Math.max(height, mMinHeight); - ViewGroup.LayoutParams lp = mActivityView.getLayoutParams(); + height = Math.max(height, mIsOverflow ? mOverflowHeight : mMinHeight); + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mTaskView.getLayoutParams(); mNeedsNewHeight = lp.height != height; - if (!mKeyboardVisible) { - // If the keyboard is visible... don't adjust the height because that will cause - // a configuration change and the keyboard will be lost. + if (!mImeVisible) { + // If the ime is visible... don't adjust the height because that will cause + // a configuration change and the ime will be lost. lp.height = (int) height; - mActivityView.setLayoutParams(lp); + mTaskView.setLayoutParams(lp); mNeedsNewHeight = false; } if (DEBUG_BUBBLE_EXPANDED_VIEW) { @@ -689,12 +547,15 @@ public class BubbleExpandedView extends LinearLayout { private int getMaxExpandedHeight() { mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize); + int expandedContainerY = mExpandedViewContainerLocation != null + ? mExpandedViewContainerLocation[1] + : 0; int bottomInset = getRootWindowInsets() != null ? getRootWindowInsets().getStableInsetBottom() : 0; return mDisplaySize.y - - mExpandedViewContainerLocation[1] + - expandedContainerY - getPaddingTop() - getPaddingBottom() - mSettingsIconHeight @@ -714,14 +575,12 @@ public class BubbleExpandedView extends LinearLayout { Log.d(TAG, "updateView: bubble=" + getBubbleKey()); } - mExpandedViewContainerLocation = containerLocationOnScreen; - - if (usingActivityView() - && mActivityView.getVisibility() == VISIBLE - && mActivityView.isAttachedToWindow()) { - mActivityView.onLocationChanged(); + if (mTaskView != null + && mTaskView.getVisibility() == VISIBLE + && mTaskView.isAttachedToWindow()) { updateHeight(); + mTaskView.onLocationChanged(); } } @@ -744,65 +603,19 @@ public class BubbleExpandedView extends LinearLayout { } /** - * Removes and releases an ActivityView if one was previously created for this bubble. + * Cleans up anything related to the task and TaskView. */ public void cleanUpExpandedState() { if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "cleanUpExpandedState: mActivityViewStatus=" + mActivityViewStatus - + ", bubble=" + getBubbleKey()); - } - if (mActivityView == null) { - return; - } - mActivityView.release(); - if (mTaskId != -1) { - try { - ActivityTaskManager.getService().removeTask(mTaskId); - } catch (RemoteException e) { - Log.w(TAG, "Failed to remove taskId " + mTaskId); - } - mTaskId = -1; - } - removeView(mActivityView); - - mActivityView = null; - } - - /** - * Called when the last task is removed from a {@link android.hardware.display.VirtualDisplay} - * which {@link ActivityView} uses. - */ - void notifyDisplayEmpty() { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "notifyDisplayEmpty: bubble=" - + getBubbleKey() - + " mActivityViewStatus=" + mActivityViewStatus); + Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); } - if (mActivityViewStatus == ActivityViewStatus.ACTIVITY_STARTED) { - mActivityViewStatus = ActivityViewStatus.INITIALIZED; + if (mTaskView != null) { + mTaskView.release(); } - } - - private boolean usingActivityView() { - return (mPendingIntent != null || mBubble.hasMetadataShortcutId()) - && mActivityView != null; - } - - /** - * @return the display id of the virtual display. - */ - public int getVirtualDisplayId() { - if (usingActivityView()) { - return mActivityView.getVirtualDisplayId(); - } - return INVALID_DISPLAY; - } - - private VirtualDisplay getVirtualDisplay() { - if (usingActivityView()) { - return mActivityView.getVirtualDisplay(); + if (mTaskView != null) { + removeView(mTaskView); + mTaskView = null; } - return null; } /** @@ -812,7 +625,6 @@ public class BubbleExpandedView extends LinearLayout { @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { pw.print("BubbleExpandedView"); pw.print(" taskId: "); pw.println(mTaskId); - pw.print(" activityViewStatus: "); pw.println(mActivityViewStatus); pw.print(" stackView: "); pw.println(mStackView); } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java index 1fa3aaae5e61..009114ffa0be 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java @@ -18,6 +18,8 @@ package com.android.systemui.bubbles; import static android.graphics.Paint.ANTI_ALIAS_FLAG; import static android.graphics.Paint.FILTER_BITMAP_FLAG; +import static com.android.systemui.Interpolators.ALPHA_IN; +import static com.android.systemui.Interpolators.ALPHA_OUT; import android.animation.ArgbEvaluator; import android.content.Context; @@ -34,6 +36,7 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.text.TextUtils; +import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -55,6 +58,11 @@ public class BubbleFlyoutView extends FrameLayout { /** Max width of the flyout, in terms of percent of the screen width. */ private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f; + /** Translation Y of fade animation. */ + private static final float FLYOUT_FADE_Y = 40f; + + private static final long FLYOUT_FADE_DURATION = 200L; + private final int mFlyoutPadding; private final int mFlyoutSpaceFromBubble; private final int mPointerSize; @@ -103,6 +111,9 @@ public class BubbleFlyoutView extends FrameLayout { /** The bounds of the flyout background, kept up to date as it transitions to the 'new' dot. */ private final RectF mBgRect = new RectF(); + /** The y position of the flyout, relative to the top of the screen. */ + private float mFlyoutY = 0f; + /** * Percent progress in the transition from flyout to 'new' dot. These two values are the inverse * of each other (if we're 40% transitioned to the dot, we're 60% flyout), but it makes the code @@ -212,18 +223,41 @@ public class BubbleFlyoutView extends FrameLayout { super.onDraw(canvas); } - /** Configures the flyout, collapsed into to dot form. */ - void setupFlyoutStartingAsDot( - Bubble.FlyoutMessage flyoutMessage, - PointF stackPos, - float parentWidth, - boolean arrowPointingLeft, - int dotColor, - @Nullable Runnable onLayoutComplete, - @Nullable Runnable onHide, - float[] dotCenter, - boolean hideDot) { + void updateFontSize(float fontScale) { + final float fontSize = mContext.getResources() + .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material); + final float newFontSize = fontSize * fontScale; + mMessageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, newFontSize); + mSenderText.setTextSize(TypedValue.COMPLEX_UNIT_PX, newFontSize); + } + + /* + * Fade animation for consecutive flyouts. + */ + void animateUpdate(Bubble.FlyoutMessage flyoutMessage, float parentWidth, float stackY) { + fade(false /* in */); + updateFlyoutMessage(flyoutMessage, parentWidth); + // Wait for TextViews to layout with updated height. + post(() -> { + mFlyoutY = stackY + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f; + fade(true /* in */); + }); + } + private void fade(boolean in) { + setAlpha(in ? 0f : 1f); + setTranslationY(in ? mFlyoutY : mFlyoutY + FLYOUT_FADE_Y); + animate() + .alpha(in ? 1f : 0f) + .setDuration(FLYOUT_FADE_DURATION) + .setInterpolator(in ? ALPHA_IN : ALPHA_OUT); + animate() + .translationY(in ? mFlyoutY : mFlyoutY - FLYOUT_FADE_Y) + .setDuration(FLYOUT_FADE_DURATION) + .setInterpolator(in ? ALPHA_IN : ALPHA_OUT); + } + + private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage, float parentWidth) { final Drawable senderAvatar = flyoutMessage.senderAvatar; if (senderAvatar != null && flyoutMessage.isGroupChat) { mSenderAvatar.setVisibility(VISIBLE); @@ -247,6 +281,27 @@ public class BubbleFlyoutView extends FrameLayout { mSenderText.setVisibility(GONE); } + // Set the flyout TextView's max width in terms of percent, and then subtract out the + // padding so that the entire flyout view will be the desired width (rather than the + // TextView being the desired width + extra padding). + mMessageText.setMaxWidth(maxTextViewWidth); + mMessageText.setText(flyoutMessage.message); + } + + /** Configures the flyout, collapsed into dot form. */ + void setupFlyoutStartingAsDot( + Bubble.FlyoutMessage flyoutMessage, + PointF stackPos, + float parentWidth, + boolean arrowPointingLeft, + int dotColor, + @Nullable Runnable onLayoutComplete, + @Nullable Runnable onHide, + float[] dotCenter, + boolean hideDot) { + + updateFlyoutMessage(flyoutMessage, parentWidth); + mArrowPointingLeft = arrowPointingLeft; mDotColor = dotColor; mOnHide = onHide; @@ -254,24 +309,12 @@ public class BubbleFlyoutView extends FrameLayout { setCollapsePercent(1f); - // Set the flyout TextView's max width in terms of percent, and then subtract out the - // padding so that the entire flyout view will be the desired width (rather than the - // TextView being the desired width + extra padding). - mMessageText.setMaxWidth(maxTextViewWidth); - mMessageText.setText(flyoutMessage.message); - - // Wait for the TextView to lay out so we know its line count. + // Wait for TextViews to layout with updated height. post(() -> { - float restingTranslationY; - // Multi line flyouts get top-aligned to the bubble. - if (mMessageText.getLineCount() > 1) { - restingTranslationY = stackPos.y + mBubbleIconTopPadding; - } else { - // Single line flyouts are vertically centered with respect to the bubble. - restingTranslationY = - stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f; - } - setTranslationY(restingTranslationY); + // Flyout is vertically centered with respect to the bubble. + mFlyoutY = + stackPos.y + (mBubbleSize - mFlyoutTextContainer.getHeight()) / 2f; + setTranslationY(mFlyoutY); // Calculate the translation required to position the flyout next to the bubble stack, // with the desired padding. @@ -291,7 +334,7 @@ public class BubbleFlyoutView extends FrameLayout { final float dotPositionY = stackPos.y + mDotCenter[1] - adjustmentForScaleAway; final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX; - final float distanceFromLayoutTopToDotCenterY = restingTranslationY - dotPositionY; + final float distanceFromLayoutTopToDotCenterY = mFlyoutY - dotPositionY; mTranslationXWhenDot = -distanceFromFlyoutLeftToDotCenterX; mTranslationYWhenDot = -distanceFromLayoutTopToDotCenterY; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java index 86ba8c5c7192..48c809d1b0a7 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java @@ -19,17 +19,22 @@ package com.android.systemui.bubbles; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; +import com.android.internal.util.FrameworkStatsLog; /** - * Interface for handling bubble-specific logging. + * Implementation of UiEventLogger for logging bubble UI events. + * + * See UiEventReported atom in atoms.proto for more context. */ -public interface BubbleLogger extends UiEventLogger { +public class BubbleLogger { + + private final UiEventLogger mUiEventLogger; /** * Bubble UI event. */ @VisibleForTesting - enum Event implements UiEventLogger.UiEventEnum { + public enum Event implements UiEventLogger.UiEventEnum { @UiEvent(doc = "User dismissed the bubble via gesture, add bubble to overflow.") BUBBLE_OVERFLOW_ADD_USER_GESTURE(483), @@ -70,23 +75,80 @@ public interface BubbleLogger extends UiEventLogger { } } + public BubbleLogger(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; + } + /** * @param b Bubble involved in this UI event * @param e UI event */ - void log(Bubble b, UiEventEnum e); + public void log(Bubble b, UiEventLogger.UiEventEnum e) { + mUiEventLogger.logWithInstanceId(e, b.getAppUid(), b.getPackageName(), b.getInstanceId()); + } /** - * * @param b Bubble removed from overflow - * @param r Reason that bubble was removed from overflow + * @param r Reason that bubble was removed */ - void logOverflowRemove(Bubble b, @BubbleController.DismissReason int r); + public void logOverflowRemove(Bubble b, @BubbleController.DismissReason int r) { + if (r == BubbleController.DISMISS_NOTIF_CANCEL) { + log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_CANCEL); + } else if (r == BubbleController.DISMISS_GROUP_CANCELLED) { + log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_GROUP_CANCEL); + } else if (r == BubbleController.DISMISS_NO_LONGER_BUBBLE) { + log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_NO_LONGER_BUBBLE); + } else if (r == BubbleController.DISMISS_BLOCKED) { + log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BLOCKED); + } + } /** - * * @param b Bubble added to overflow * @param r Reason that bubble was added to overflow */ - void logOverflowAdd(Bubble b, @BubbleController.DismissReason int r); -} + public void logOverflowAdd(Bubble b, @BubbleController.DismissReason int r) { + if (r == BubbleController.DISMISS_AGED) { + log(b, Event.BUBBLE_OVERFLOW_ADD_AGED); + } else if (r == BubbleController.DISMISS_USER_GESTURE) { + log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE); + } + } + + void logStackUiChanged(String packageName, int action, int bubbleCount, float normalX, + float normalY) { + FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_UI_CHANGED, + packageName, + null /* notification channel */, + 0 /* notification ID */, + 0 /* bubble position */, + bubbleCount, + action, + normalX, + normalY, + false /* unread bubble */, + false /* on-going bubble */, + false /* isAppForeground (unused) */); + } + + void logShowOverflow(String packageName, int currentUserId) { + mUiEventLogger.log(BubbleLogger.Event.BUBBLE_OVERFLOW_SELECTED, currentUserId, + packageName); + } + + void logBubbleUiChanged(Bubble bubble, String packageName, int action, int bubbleCount, + float normalX, float normalY, int index) { + FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_UI_CHANGED, + packageName, + bubble.getChannelId() /* notification channel */, + bubble.getNotificationId() /* notification ID */, + index, + bubbleCount, + action, + normalX, + normalY, + bubble.showInShade() /* isUnread */, + false /* isOngoing (unused) */, + false /* isAppForeground (unused) */); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java deleted file mode 100644 index ea612af3d4a4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2020 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.bubbles; - -import android.os.UserHandle; - -import com.android.internal.logging.UiEventLoggerImpl; -import com.android.systemui.shared.system.SysUiStatsLog; - -/** - * Implementation of UiEventLogger for logging bubble UI events. - * - * See UiEventReported atom in atoms.proto for more context. - */ -public class BubbleLoggerImpl extends UiEventLoggerImpl implements BubbleLogger { - - /** - * @param b Bubble involved in this UI event - * @param e UI event - */ - public void log(Bubble b, UiEventEnum e) { - logWithInstanceId(e, b.getAppUid(), b.getPackageName(), b.getInstanceId()); - } - - /** - * @param b Bubble removed from overflow - * @param r Reason that bubble was removed - */ - public void logOverflowRemove(Bubble b, @BubbleController.DismissReason int r) { - if (r == BubbleController.DISMISS_NOTIF_CANCEL) { - log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_CANCEL); - } else if (r == BubbleController.DISMISS_GROUP_CANCELLED) { - log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_GROUP_CANCEL); - } else if (r == BubbleController.DISMISS_NO_LONGER_BUBBLE) { - log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_NO_LONGER_BUBBLE); - } else if (r == BubbleController.DISMISS_BLOCKED) { - log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BLOCKED); - } - } - - /** - * @param b Bubble added to overflow - * @param r Reason that bubble was added to overflow - */ - public void logOverflowAdd(Bubble b, @BubbleController.DismissReason int r) { - if (r == BubbleController.DISMISS_AGED) { - log(b, Event.BUBBLE_OVERFLOW_ADD_AGED); - } else if (r == BubbleController.DISMISS_USER_GESTURE) { - log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE); - } - } - - void logStackUiChanged(String packageName, int action, int bubbleCount, float normalX, - float normalY) { - SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED, - packageName, - null /* notification channel */, - 0 /* notification ID */, - 0 /* bubble position */, - bubbleCount, - action, - normalX, - normalY, - false /* unread bubble */, - false /* on-going bubble */, - false /* isAppForeground (unused) */); - } - - void logShowOverflow(String packageName, int currentUserId) { - super.log(BubbleLogger.Event.BUBBLE_OVERFLOW_SELECTED, currentUserId, - packageName); - } - - void logBubbleUiChanged(Bubble bubble, String packageName, int action, int bubbleCount, - float normalX, float normalY, int index) { - SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED, - packageName, - bubble.getChannelId() /* notification channel */, - bubble.getNotificationId() /* notification ID */, - index, - bubbleCount, - action, - normalX, - normalY, - bubble.showInShade() /* isUnread */, - false /* isOngoing (unused) */, - false /* isAppForeground (unused) */); - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.kt index bf7c860132bf..102055de2bea 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.kt @@ -16,6 +16,7 @@ package com.android.systemui.bubbles +import android.app.ActivityTaskManager.INVALID_TASK_ID import android.content.Context import android.content.res.Configuration import android.graphics.Bitmap @@ -37,8 +38,8 @@ class BubbleOverflow( private val stack: BubbleStackView ) : BubbleViewProvider { - private lateinit var bitmap : Bitmap - private lateinit var dotPath : Path + private lateinit var bitmap: Bitmap + private lateinit var dotPath: Path private var bitmapSize = 0 private var iconBitmapSize = 0 @@ -167,8 +168,8 @@ class BubbleOverflow( return KEY } - override fun getDisplayId(): Int { - return expandedView.virtualDisplayId + override fun getTaskId(): Int { + return if (expandedView != null) expandedView.getTaskId() else INVALID_TASK_ID } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java index 160addc405fa..fc3f5b6cbf5e 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java @@ -22,11 +22,13 @@ import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.os.Bundle; +import android.os.IBinder; import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; @@ -47,20 +49,21 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -import javax.inject.Inject; /** * Activity for showing aged out bubbles. * Must be public to be accessible to androidx...AppComponentFactory */ public class BubbleOverflowActivity extends Activity { + static final String EXTRA_BUBBLE_CONTROLLER = "bubble_controller"; + private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES; private LinearLayout mEmptyState; private TextView mEmptyStateTitle; private TextView mEmptyStateSubtitle; private ImageView mEmptyStateImage; - private BubbleController mBubbleController; + private Bubbles mBubbles; private BubbleOverflowAdapter mAdapter; private RecyclerView mRecyclerView; private List<Bubble> mOverflowBubbles = new ArrayList<>(); @@ -71,7 +74,8 @@ public class BubbleOverflowActivity extends Activity { } @Override public boolean canScrollVertically() { - if (mBubbleController.inLandscape()) { + if (getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE) { return super.canScrollVertically(); } return false; @@ -92,11 +96,6 @@ public class BubbleOverflowActivity extends Activity { } } - @Inject - public BubbleOverflowActivity(BubbleController controller) { - mBubbleController = controller; - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -108,6 +107,15 @@ public class BubbleOverflowActivity extends Activity { mEmptyStateSubtitle = findViewById(R.id.bubble_overflow_empty_subtitle); mEmptyStateImage = findViewById(R.id.bubble_overflow_empty_state_image); + Intent intent = getIntent(); + if (intent != null && intent.getExtras() != null) { + IBinder binder = intent.getExtras().getBinder(EXTRA_BUBBLE_CONTROLLER); + if (binder instanceof ObjectWrapper) { + mBubbles = ((ObjectWrapper<Bubbles>) binder).get(); + } + } else { + Log.w(TAG, "Bubble overflow activity created without bubble controller!"); + } updateOverflow(); } @@ -131,15 +139,15 @@ public class BubbleOverflowActivity extends Activity { final int viewHeight = recyclerViewHeight / rows; mAdapter = new BubbleOverflowAdapter(getApplicationContext(), mOverflowBubbles, - mBubbleController::promoteBubbleFromOverflow, viewWidth, viewHeight); + mBubbles::promoteBubbleFromOverflow, viewWidth, viewHeight); mRecyclerView.setAdapter(mAdapter); mOverflowBubbles.clear(); - mOverflowBubbles.addAll(mBubbleController.getOverflowBubbles()); + mOverflowBubbles.addAll(mBubbles.getOverflowBubbles()); mAdapter.notifyDataSetChanged(); updateEmptyStateVisibility(); - mBubbleController.setOverflowListener(mDataListener); + mBubbles.setOverflowListener(mDataListener); updateTheme(); } @@ -209,8 +217,7 @@ public class BubbleOverflowActivity extends Activity { if (DEBUG_OVERFLOW) { Log.d(TAG, BubbleDebugConfig.formatBubblesString( - mBubbleController.getOverflowBubbles(), - null)); + mBubbles.getOverflowBubbles(), null)); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index f2fba23564da..431719f98ad9 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -16,11 +16,17 @@ package com.android.systemui.bubbles; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; +import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.SuppressLint; -import android.app.ActivityView; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -67,21 +73,16 @@ import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; import com.android.systemui.Interpolators; -import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.bubbles.animation.AnimatableScaleMatrix; import com.android.systemui.bubbles.animation.ExpandedAnimationController; import com.android.systemui.bubbles.animation.PhysicsAnimationLayout; import com.android.systemui.bubbles.animation.StackAnimationController; -import com.android.systemui.model.SysUiState; -import com.android.systemui.shared.system.QuickStepContract; -import com.android.systemui.shared.system.SysUiStatsLog; -import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; -import com.android.systemui.util.FloatingContentCoordinator; -import com.android.systemui.util.RelativeTouchListener; -import com.android.systemui.util.animation.PhysicsAnimator; -import com.android.systemui.util.magnetictarget.MagnetizedObject; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -92,12 +93,6 @@ import java.util.Collections; import java.util.List; import java.util.function.Consumer; -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; - /** * Renders bubbles in a stack and handles animating expanded and collapsed states. */ @@ -120,6 +115,8 @@ public class BubbleStackView extends FrameLayout /** Duration of the flyout alpha animations. */ private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100; + private static final int FADE_IN_DURATION = 320; + /** Percent to darken the bubbles when they're in the dismiss target. */ private static final float DARKEN_PERCENT = 0.3f; @@ -301,7 +298,7 @@ public class BubbleStackView extends FrameLayout pw.println(" expandedViewAlpha: " + expandedView.getAlpha()); pw.println(" expandedViewTaskId: " + expandedView.getTaskId()); - final ActivityView av = expandedView.getActivityView(); + final View av = expandedView.getTaskView(); if (av != null) { pw.println(" activityViewVis: " + av.getVisibility()); @@ -322,8 +319,6 @@ public class BubbleStackView extends FrameLayout /** Callback to run when we want to unbubble the given notification's conversation. */ private Consumer<String> mUnbubbleConversationCallback; - private SysUiState mSysUiState; - private boolean mViewUpdatedRequested = false; private boolean mIsExpansionAnimating = false; private boolean mIsBubbleSwitchAnimating = false; @@ -331,8 +326,6 @@ public class BubbleStackView extends FrameLayout /** The view to desaturate/darken when magneted to the dismiss target. */ @Nullable private View mDesaturateAndDarkenTargetView; - private LayoutInflater mInflater; - private Rect mTempRect = new Rect(); private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); @@ -400,6 +393,11 @@ public class BubbleStackView extends FrameLayout public final Consumer<Boolean> mOnImeVisibilityChanged; /** + * Callback to run when the bubble expand status changes. + */ + private final Consumer<Boolean> mOnBubbleExpandChanged; + + /** * Callback to run to ask BubbleController to hide the current IME. */ private final Runnable mHideCurrentInputMethodCallback; @@ -659,7 +657,7 @@ public class BubbleStackView extends FrameLayout viewInitialX + dx, velX, velY) <= 0; updateBubbleIcons(); logBubbleEvent(null /* no bubble associated with bubble stack move */, - SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); } mDismissView.hide(); } @@ -741,16 +739,13 @@ public class BubbleStackView extends FrameLayout public BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState, Runnable allBubblesAnimatedOutAction, Consumer<Boolean> onImeVisibilityChanged, - Runnable hideCurrentInputMethodCallback) { + Runnable hideCurrentInputMethodCallback, + Consumer<Boolean> onBubbleExpandChanged) { super(context); mBubbleData = data; - mInflater = LayoutInflater.from(context); - - mSysUiState = sysUiState; Resources res = getResources(); mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); @@ -864,21 +859,13 @@ public class BubbleStackView extends FrameLayout mOnImeVisibilityChanged = onImeVisibilityChanged; mHideCurrentInputMethodCallback = hideCurrentInputMethodCallback; + mOnBubbleExpandChanged = onBubbleExpandChanged; setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { onImeVisibilityChanged.accept(insets.getInsets(WindowInsets.Type.ime()).bottom > 0); - if (!mIsExpanded || mIsExpansionAnimating) { return view.onApplyWindowInsets(insets); } - mExpandedAnimationController.updateYPosition( - // Update the insets after we're done translating otherwise position - // calculation for them won't be correct. - () -> { - if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().updateInsets(insets); - } - }); return view.onApplyWindowInsets(insets); }); @@ -973,7 +960,7 @@ public class BubbleStackView extends FrameLayout animate() .setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED) - .setDuration(CollapsedStatusBarFragment.FADE_IN_DURATION); + .setDuration(FADE_IN_DURATION); } /** @@ -1065,7 +1052,7 @@ public class BubbleStackView extends FrameLayout mBubbleData.setExpanded(false); mContext.startActivityAsUser(intent, bubble.getUser()); logBubbleEvent(bubble, - SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); } }); @@ -1081,8 +1068,7 @@ public class BubbleStackView extends FrameLayout * Whether the educational view should show for the expanded view "manage" menu. */ private boolean shouldShowManageEdu() { - final boolean seen = Prefs.getBoolean(mContext, - Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false /* default */); + final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) && mExpandedBubble != null; if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { @@ -1106,8 +1092,7 @@ public class BubbleStackView extends FrameLayout * Whether education view should show for the collapsed stack. */ private boolean shouldShowStackEdu() { - final boolean seen = Prefs.getBoolean(getContext(), - Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION, false /* default */); + final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext); if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { Log.d(TAG, "Show stack edu: " + shouldShow); @@ -1115,6 +1100,11 @@ public class BubbleStackView extends FrameLayout return shouldShow; } + private boolean getPrefBoolean(String key) { + return mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE) + .getBoolean(key, false /* default */); + } + /** * @return true if education view for collapsed stack should show and was not showing before. */ @@ -1155,6 +1145,10 @@ public class BubbleStackView extends FrameLayout addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); } + void updateFlyout(float fontScale) { + mFlyout.updateFontSize(fontScale); + } + private void updateOverflow() { mBubbleOverflow.update(); mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), @@ -1249,7 +1243,15 @@ public class BubbleStackView extends FrameLayout mTempRect.setEmpty(); getTouchableRegion(mTempRect); - inoutInfo.touchableRegion.set(mTempRect); + if (mIsExpanded && mExpandedBubble != null + && mExpandedBubble.getExpandedView() != null + && mExpandedBubble.getExpandedView().getTaskView() != null) { + inoutInfo.touchableRegion.set(mTempRect); + mExpandedBubble.getExpandedView().getTaskView().getBoundsOnScreen(mTempRect); + inoutInfo.touchableRegion.op(mTempRect, Region.Op.DIFFERENCE); + } else { + inoutInfo.touchableRegion.set(mTempRect); + } } @Override @@ -1437,13 +1439,6 @@ public class BubbleStackView extends FrameLayout } /** - * The {@link BadgedImageView} that is expanded, null if one does not exist. - */ - View getExpandedBubbleView() { - return mExpandedBubble != null ? mExpandedBubble.getIconView() : null; - } - - /** * The {@link Bubble} that is expanded, null if one does not exist. */ @Nullable @@ -1483,7 +1478,7 @@ public class BubbleStackView extends FrameLayout new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); animateInFlyoutForBubble(bubble); requestUpdate(); - logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); } // via BubbleData.Listener @@ -1503,7 +1498,7 @@ public class BubbleStackView extends FrameLayout bubble.cleanupViews(); } updatePointerPosition(); - logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; } } @@ -1521,7 +1516,7 @@ public class BubbleStackView extends FrameLayout void updateBubble(Bubble bubble) { animateInFlyoutForBubble(bubble); requestUpdate(); - logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); } public void updateBubbleOrder(List<Bubble> bubbles) { @@ -1558,7 +1553,7 @@ public class BubbleStackView extends FrameLayout return; } - if (bubbleToSelect.getKey() == BubbleOverflow.KEY) { + if (bubbleToSelect.getKey().equals(BubbleOverflow.KEY)) { mBubbleData.setShowingOverflow(true); } else { mBubbleData.setShowingOverflow(false); @@ -1617,8 +1612,9 @@ public class BubbleStackView extends FrameLayout requestUpdate(); logBubbleEvent(previouslySelected, - SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); - logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); + logBubbleEvent(bubbleToSelect, + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); notifyExpansionChanged(previouslySelected, false /* expanded */); notifyExpansionChanged(bubbleToSelect, true /* expanded */); }); @@ -1648,30 +1644,21 @@ public class BubbleStackView extends FrameLayout hideCurrentInputMethod(); - mSysUiState - .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) - .commitUpdate(mContext.getDisplayId()); + mOnBubbleExpandChanged.accept(shouldExpand); if (mIsExpanded) { animateCollapse(); - logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); + logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); } else { animateExpansion(); // TODO: move next line to BubbleData - logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); - logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); + logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); + logBubbleEvent(mExpandedBubble, + FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); } notifyExpansionChanged(mExpandedBubble, mIsExpanded); } - void showExpandedViewContents(int displayId) { - if (mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null - && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) { - mExpandedBubble.setContentVisibility(true); - } - } - /** * Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or * not. @@ -1706,7 +1693,6 @@ public class BubbleStackView extends FrameLayout updateOverflowVisibility(); updatePointerPosition(); mExpandedAnimationController.expandFromStack(() -> { - afterExpandedViewAnimation(); if (mIsExpanded && mExpandedBubble.getExpandedView() != null) { maybeShowManageEdu(); } @@ -1769,11 +1755,10 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainerMatrix); }) .withEndActions(() -> { + afterExpandedViewAnimation(); if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { mExpandedBubble.getExpandedView() - .setContentVisibility(true); - mExpandedBubble.getExpandedView() .setSurfaceZOrderedOnTop(false); } }) @@ -1917,7 +1902,6 @@ public class BubbleStackView extends FrameLayout }) .withEndActions(() -> { if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().setContentVisibility(true); mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); } @@ -1953,13 +1937,6 @@ public class BubbleStackView extends FrameLayout } } - /** Return the BubbleView at the given index from the bubble container. */ - public BadgedImageView getBubbleAt(int i) { - return getBubbleCount() > i - ? (BadgedImageView) mBubbleContainer.getChildAt(i) - : null; - } - /** Moves the bubbles out of the way if they're going to be over the keyboard. */ public void onImeVisibilityChanged(boolean visible, int height) { mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0); @@ -1981,6 +1958,9 @@ public class BubbleStackView extends FrameLayout FLYOUT_IME_ANIMATION_SPRING_CONFIG) .start(); } + } else if (mIsExpanded && mExpandedBubble != null + && mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView().setImeVisible(visible); } } @@ -2191,11 +2171,7 @@ public class BubbleStackView extends FrameLayout return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop; } - /** - * Animates in the flyout for the given bubble, if available, and then hides it after some time. - */ - @VisibleForTesting - void animateInFlyoutForBubble(Bubble bubble) { + private boolean shouldShowFlyout(Bubble bubble) { Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage(); final BadgedImageView bubbleView = bubble.getIconView(); if (flyoutMessage == null @@ -2207,11 +2183,22 @@ public class BubbleStackView extends FrameLayout || mIsGestureInProgress || mBubbleToExpandAfterFlyoutCollapse != null || bubbleView == null) { - if (bubbleView != null) { + if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) { bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); } // Skip the message if none exists, we're expanded or animating expansion, or we're // about to expand a bubble from the previous tapped flyout, or if bubble view is null. + return false; + } + return true; + } + + /** + * Animates in the flyout for the given bubble, if available, and then hides it after some time. + */ + @VisibleForTesting + void animateInFlyoutForBubble(Bubble bubble) { + if (!shouldShowFlyout(bubble)) { return; } @@ -2229,25 +2216,22 @@ public class BubbleStackView extends FrameLayout } // Stop suppressing the dot now that the flyout has morphed into the dot. - bubbleView.removeDotSuppressionFlag( + bubble.getIconView().removeDotSuppressionFlag( BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); - mFlyout.setVisibility(INVISIBLE); - // Hide the stack after a delay, if needed. updateTemporarilyInvisibleAnimation(false /* hideImmediately */); }; - mFlyout.setVisibility(INVISIBLE); // Suppress the dot when we are animating the flyout. - bubbleView.addDotSuppressionFlag( + bubble.getIconView().addDotSuppressionFlag( BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0. post(() -> { // An auto-expanding bubble could have been posted during the time it takes to // layout. - if (isExpanded()) { + if (isExpanded() || bubble.getIconView() == null) { return; } final Runnable expandFlyoutAfterDelay = () -> { @@ -2264,23 +2248,26 @@ public class BubbleStackView extends FrameLayout mFlyout.postDelayed(mAnimateInFlyout, 200); }; - if (bubble.getIconView() == null) { - return; - } - mFlyout.setupFlyoutStartingAsDot(flyoutMessage, - mStackAnimationController.getStackPosition(), getWidth(), - mStackAnimationController.isStackOnLeftSide(), - bubble.getIconView().getDotColor() /* dotColor */, - expandFlyoutAfterDelay /* onLayoutComplete */, - mAfterFlyoutHidden, - bubble.getIconView().getDotCenter(), - !bubble.showDot()); + if (mFlyout.getVisibility() == View.VISIBLE) { + mFlyout.animateUpdate(bubble.getFlyoutMessage(), getWidth(), + mStackAnimationController.getStackPosition().y); + } else { + mFlyout.setVisibility(INVISIBLE); + mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(), + mStackAnimationController.getStackPosition(), getWidth(), + mStackAnimationController.isStackOnLeftSide(), + bubble.getIconView().getDotColor() /* dotColor */, + expandFlyoutAfterDelay /* onLayoutComplete */, + mAfterFlyoutHidden, + bubble.getIconView().getDotCenter(), + !bubble.showDot()); + } mFlyout.bringToFront(); }); mFlyout.removeCallbacks(mHideFlyout); mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); - logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); + logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); } /** Hide the flyout immediately and cancel any pending hide runnables. */ @@ -2388,7 +2375,6 @@ public class BubbleStackView extends FrameLayout final float targetY = mTempRect.bottom - mManageMenu.getHeight(); final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f; - if (show) { mManageMenu.setScaleX(0.5f); mManageMenu.setScaleY(0.5f); @@ -2405,6 +2391,8 @@ public class BubbleStackView extends FrameLayout .withEndActions(() -> { View child = mManageMenu.getChildAt(0); child.requestAccessibilityFocus(); + // Update the AV's obscured touchable region for the new visibility state. + mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); }) .start(); @@ -2416,12 +2404,15 @@ public class BubbleStackView extends FrameLayout .spring(DynamicAnimation.SCALE_Y, 0.5f) .spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation) .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4f) - .withEndActions(() -> mManageMenu.setVisibility(View.INVISIBLE)) + .withEndActions(() -> { + mManageMenu.setVisibility(View.INVISIBLE); + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + // Update the AV's obscured touchable region for the new state. + mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + } + }) .start(); } - - // Update the AV's obscured touchable region for the new menu visibility state. - mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); } private void updateExpandedBubble() { @@ -2441,7 +2432,6 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setAlpha(0f); mExpandedViewContainer.addView(bev); bev.setManageClickListener((view) -> showManageMenu(!mShowingManage)); - bev.populateExpandedView(); if (!mIsExpansionAnimating) { mSurfaceSynchronizer.syncSurfaceAndRun(() -> { @@ -2498,7 +2488,7 @@ public class BubbleStackView extends FrameLayout mAnimatingOutSurfaceContainer.setTranslationY(0); final int[] activityViewLocation = - mExpandedBubble.getExpandedView().getActivityViewLocationOnScreen(); + mExpandedBubble.getExpandedView().getTaskViewLocationOnScreen(); final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen(); // Translate the surface to overlap the real ActivityView. @@ -2674,17 +2664,6 @@ public class BubbleStackView extends FrameLayout getNormalizedYPosition()); } - /** - * Called when a back gesture should be directed to the Bubbles stack. When expanded, - * a back key down/up event pair is forwarded to the bubble Activity. - */ - boolean performBackPressIfNeeded() { - if (!isExpanded() || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { - return false; - } - return mExpandedBubble.getExpandedView().performBackPressIfNeeded(); - } - /** For debugging only */ List<Bubble> getBubblesOnScreen() { List<Bubble> bubbles = new ArrayList<>(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTaskView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTaskView.java deleted file mode 100644 index 06205c5c1c41..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTaskView.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (C) 2020 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.bubbles; - -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; -import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityOptions; -import android.app.PendingIntent; -import android.window.TaskEmbedder; -import android.window.TaskOrganizerTaskEmbedder; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.graphics.Matrix; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.Region; -import android.view.IWindow; -import android.view.SurfaceControl; -import android.view.SurfaceHolder; -import android.view.SurfaceView; - -import dalvik.system.CloseGuard; - - -public class BubbleTaskView extends SurfaceView implements SurfaceHolder.Callback, - TaskEmbedder.Host { - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleTaskView" : TAG_BUBBLES; - - private final CloseGuard mGuard = CloseGuard.get(); - private boolean mOpened; // Protected by mGuard. - - private TaskEmbedder mTaskEmbedder; - private final SurfaceControl.Transaction mTmpTransaction = new SurfaceControl.Transaction(); - private final Rect mTmpRect = new Rect(); - - public BubbleTaskView(Context context) { - super(context); - - mTaskEmbedder = new TaskOrganizerTaskEmbedder(context, this); - setUseAlpha(); - getHolder().addCallback(this); - - mOpened = true; - mGuard.open("release"); - } - - public void setCallback(TaskEmbedder.Listener callback) { - if (callback == null) { - mTaskEmbedder.setListener(null); - return; - } - mTaskEmbedder.setListener(callback); - } - - public void startShortcutActivity(@NonNull ShortcutInfo shortcut, - @NonNull ActivityOptions options, @Nullable Rect sourceBounds) { - mTaskEmbedder.startShortcutActivity(shortcut, options, sourceBounds); - } - - public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, - @NonNull ActivityOptions options) { - mTaskEmbedder.startActivity(pendingIntent, fillInIntent, options); - } - - public void onLocationChanged() { - mTaskEmbedder.notifyBoundsChanged(); - } - - @Override - public Rect getScreenBounds() { - getBoundsOnScreen(mTmpRect); - return mTmpRect; - } - - @Override - public void onTaskBackgroundColorChanged(TaskEmbedder ts, int bgColor) { - setResizeBackgroundColor(bgColor); - } - - @Override - public Region getTapExcludeRegion() { - // Not used - return null; - } - - @Override - public Matrix getScreenToTaskMatrix() { - // Not used - return null; - } - - @Override - public IWindow getWindow() { - // Not used - return null; - } - - @Override - public Point getPositionInWindow() { - // Not used - return null; - } - - @Override - public boolean canReceivePointerEvents() { - // Not used - return false; - } - - public void release() { - if (!mTaskEmbedder.isInitialized()) { - throw new IllegalStateException( - "Trying to release container that is not initialized."); - } - performRelease(); - } - - @Override - protected void finalize() throws Throwable { - try { - if (mGuard != null) { - mGuard.warnIfOpen(); - performRelease(); - } - } finally { - super.finalize(); - } - } - - private void performRelease() { - if (!mOpened) { - return; - } - getHolder().removeCallback(this); - mTaskEmbedder.release(); - mTaskEmbedder.setListener(null); - - mGuard.close(); - mOpened = false; - } - - @Override - public void surfaceCreated(SurfaceHolder holder) { - if (!mTaskEmbedder.isInitialized()) { - mTaskEmbedder.initialize(getSurfaceControl()); - } else { - mTmpTransaction.reparent(mTaskEmbedder.getSurfaceControl(), - getSurfaceControl()).apply(); - } - mTaskEmbedder.start(); - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - mTaskEmbedder.resizeTask(width, height); - mTaskEmbedder.notifyBoundsChanged(); - } - - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - mTaskEmbedder.stop(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java index 28757facc220..010a29e3560a 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java @@ -22,8 +22,6 @@ import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import android.annotation.NonNull; -import android.app.Notification; -import android.app.Person; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -36,8 +34,6 @@ import android.graphics.Path; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.AsyncTask; -import android.os.Parcelable; -import android.text.TextUtils; import android.util.Log; import android.util.PathParser; import android.view.LayoutInflater; @@ -47,10 +43,8 @@ import androidx.annotation.Nullable; import com.android.internal.graphics.ColorUtils; import com.android.launcher3.icons.BitmapInfo; import com.android.systemui.R; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.lang.ref.WeakReference; -import java.util.List; import java.util.Objects; /** @@ -208,73 +202,6 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask } } - - /** - * Returns our best guess for the most relevant text summary of the latest update to this - * notification, based on its type. Returns null if there should not be an update message. - */ - @NonNull - static Bubble.FlyoutMessage extractFlyoutMessage(NotificationEntry entry) { - Objects.requireNonNull(entry); - final Notification underlyingNotif = entry.getSbn().getNotification(); - final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); - - Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage(); - bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean( - Notification.EXTRA_IS_GROUP_CONVERSATION); - try { - if (Notification.BigTextStyle.class.equals(style)) { - // Return the big text, it is big so probably important. If it's not there use the - // normal text. - CharSequence bigText = - underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); - bubbleMessage.message = !TextUtils.isEmpty(bigText) - ? bigText - : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); - return bubbleMessage; - } else if (Notification.MessagingStyle.class.equals(style)) { - final List<Notification.MessagingStyle.Message> messages = - Notification.MessagingStyle.Message.getMessagesFromBundleArray( - (Parcelable[]) underlyingNotif.extras.get( - Notification.EXTRA_MESSAGES)); - - final Notification.MessagingStyle.Message latestMessage = - Notification.MessagingStyle.findLatestIncomingMessage(messages); - if (latestMessage != null) { - bubbleMessage.message = latestMessage.getText(); - Person sender = latestMessage.getSenderPerson(); - bubbleMessage.senderName = sender != null ? sender.getName() : null; - bubbleMessage.senderAvatar = null; - bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null; - return bubbleMessage; - } - } else if (Notification.InboxStyle.class.equals(style)) { - CharSequence[] lines = - underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); - - // Return the last line since it should be the most recent. - if (lines != null && lines.length > 0) { - bubbleMessage.message = lines[lines.length - 1]; - return bubbleMessage; - } - } else if (Notification.MediaStyle.class.equals(style)) { - // Return nothing, media updates aren't typically useful as a text update. - return bubbleMessage; - } else { - // Default to text extra. - bubbleMessage.message = - underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); - return bubbleMessage; - } - } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { - // No use crashing, we'll just return null and the caller will assume there's no update - // message. - e.printStackTrace(); - } - - return bubbleMessage; - } - @Nullable static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { Objects.requireNonNull(context); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java index 916ad18b2812..5cc24ce5a775 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java @@ -48,5 +48,5 @@ interface BubbleViewProvider { boolean showDot(); - int getDisplayId(); + int getTaskId(); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubbles.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubbles.java new file mode 100644 index 000000000000..39c750de28ac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubbles.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 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.bubbles; + +import android.annotation.NonNull; + +import androidx.annotation.MainThread; + +import com.android.systemui.statusbar.ScrimView; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.phone.ScrimController; + +import java.util.List; + +/** + * Interface to engage bubbles feature. + */ +public interface Bubbles { + + /** + * @return {@code true} if there is a bubble associated with the provided key and if its + * notification is hidden from the shade or there is a group summary associated with the + * provided key that is hidden from the shade because it has been dismissed but still has child + * bubbles active. + */ + boolean isBubbleNotificationSuppressedFromShade(NotificationEntry entry); + + /** + * @return {@code true} if the current notification entry same as selected bubble + * notification entry and the stack is currently expanded. + */ + boolean isBubbleExpanded(NotificationEntry entry); + + /** @return {@code true} if stack of bubbles is expanded or not. */ + boolean isStackExpanded(); + + /** + * @return the {@link ScrimView} drawn behind the bubble stack. This is managed by + * {@link ScrimController} since we want the scrim's appearance and behavior to be identical to + * that of the notification shade scrim. + */ + ScrimView getScrimForBubble(); + + /** @return Bubbles for updating overflow. */ + List<Bubble> getOverflowBubbles(); + + /** Tell the stack of bubbles to collapse. */ + void collapseStack(); + + /** + * Request the stack expand if needed, then select the specified Bubble as current. + * If no bubble exists for this entry, one is created. + * + * @param entry the notification for the bubble to be selected + */ + void expandStackAndSelectBubble(NotificationEntry entry); + + /** Promote the provided bubbles when overflow view. */ + void promoteBubbleFromOverflow(Bubble bubble); + + /** + * We intercept notification entries (including group summaries) dismissed by the user when + * there is an active bubble associated with it. We do this so that developers can still + * cancel it (and hence the bubbles associated with it). However, these intercepted + * notifications should then be hidden from the shade since the user has cancelled them, so we + * {@link Bubble#setSuppressNotification}. For the case of suppressed summaries, we also add + * {@link BubbleData#addSummaryToSuppress}. + * + * @return true if we want to intercept the dismissal of the entry, else false. + */ + boolean handleDismissalInterception(NotificationEntry entry); + + /** + * Removes the bubble with the given key. + * <p> + * Must be called from the main thread. + */ + @MainThread + void removeBubble(String key, int reason); + + + /** + * When a notification is marked Priority, expand the stack if needed, + * then (maybe create and) select the given bubble. + * + * @param entry the notification for the bubble to show + */ + void onUserChangedImportance(NotificationEntry entry); + + /** + * Called when the status bar has become visible or invisible (either permanently or + * temporarily). + */ + void onStatusBarVisibilityChanged(boolean visible); + + /** + * Called when a user has indicated that an active notification should be shown as a bubble. + * <p> + * This method will collapse the shade, create the bubble without a flyout or dot, and suppress + * the notification from appearing in the shade. + * + * @param entry the notification to change bubble state for. + * @param shouldBubble whether the notification should show as a bubble or not. + */ + void onUserChangedBubble(@NonNull NotificationEntry entry, boolean shouldBubble); + + + /** See {@link BubbleController.NotifCallback}. */ + void addNotifCallback(BubbleController.NotifCallback callback); + + /** Set a listener to be notified of bubble expand events. */ + void setExpandListener(BubbleController.BubbleExpandListener listener); + + /** Set a listener to be notified of when overflow view update. */ + void setOverflowListener(BubbleData.Listener listener); + + /** The task listener for events in bubble tasks. **/ + MultiWindowTaskListener getTaskManager(); +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/DismissView.kt b/packages/SystemUI/src/com/android/systemui/bubbles/DismissView.kt index 71faf4a2eeb7..b3c552d24dcd 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/DismissView.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/DismissView.kt @@ -10,8 +10,8 @@ import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW import com.android.systemui.R -import com.android.systemui.util.DismissCircleView -import com.android.systemui.util.animation.PhysicsAnimator +import com.android.wm.shell.common.DismissCircleView +import com.android.wm.shell.animation.PhysicsAnimator /* * View that handles interactions between DismissCircleView and BubbleStackView. @@ -29,7 +29,7 @@ class DismissView(context: Context) : FrameLayout(context) { var isShowing = false private val animator = PhysicsAnimator.getInstance(circle) - private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY); + private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) private val DISMISS_SCRIM_FADE_MS = 200 init { setLayoutParams(LayoutParams( diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/ManageEducationView.kt b/packages/SystemUI/src/com/android/systemui/bubbles/ManageEducationView.kt index 26a9773f9bb8..3db07c227d02 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/ManageEducationView.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/ManageEducationView.kt @@ -25,8 +25,6 @@ import android.widget.LinearLayout import android.widget.TextView import com.android.internal.util.ContrastColorUtil import com.android.systemui.Interpolators -import com.android.systemui.Prefs -import com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION import com.android.systemui.R /** @@ -38,8 +36,8 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleManageEducationView" else BubbleDebugConfig.TAG_BUBBLES - private val ANIMATE_DURATION : Long = 200 - private val ANIMATE_DURATION_SHORT : Long = 40 + private val ANIMATE_DURATION: Long = 200 + private val ANIMATE_DURATION_SHORT: Long = 40 private val manageView by lazy { findViewById<View>(R.id.manage_education_view) } private val manageButton by lazy { findViewById<Button>(R.id.manage) } @@ -50,7 +48,7 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) private var isHiding = false init { - LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this); + LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this) visibility = View.GONE elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat() @@ -95,7 +93,7 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) * * @param show whether the user education view should show or not. */ - fun show(expandedView: BubbleExpandedView, rect : Rect) { + fun show(expandedView: BubbleExpandedView, rect: Rect) { if (visibility == VISIBLE) return alpha = 0f @@ -136,10 +134,13 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) .withEndAction { isHiding = false visibility = GONE - }; + } } private fun setShouldShow(shouldShow: Boolean) { - Prefs.putBoolean(context, HAS_SEEN_BUBBLES_MANAGE_EDUCATION, !shouldShow) + context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit().putBoolean(PREF_MANAGED_EDUCATION, !shouldShow).apply() } -}
\ No newline at end of file +} + +const val PREF_MANAGED_EDUCATION: String = "HasSeenBubblesManageOnboarding"
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/MultiWindowTaskListener.java b/packages/SystemUI/src/com/android/systemui/bubbles/MultiWindowTaskListener.java new file mode 100644 index 000000000000..dff8becccb86 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/MultiWindowTaskListener.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2020 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.bubbles; + +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_MULTI_WINDOW; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityTaskManager; +import android.os.Handler; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; +import android.view.SurfaceControl; +import android.window.TaskOrganizer; +import android.window.WindowContainerToken; + +import com.android.wm.shell.ShellTaskOrganizer; + +/** + * Manages tasks that are displayed in multi-window (e.g. bubbles). These are displayed in a + * {@link TaskView}. + * + * This class listens on {@link TaskOrganizer} callbacks for events. Once visible, these tasks will + * intercept back press events. + * + * @see android.app.WindowConfiguration#WINDOWING_MODE_MULTI_WINDOW + * @see TaskView + */ +// TODO: Place in com.android.wm.shell vs. com.android.wm.shell.bubbles on shell migration. +public class MultiWindowTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = MultiWindowTaskListener.class.getSimpleName(); + + private static final boolean DEBUG = false; + + //TODO(b/170153209): Have shell listener allow per task registration and remove this. + public interface Listener { + void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash); + void onTaskVanished(RunningTaskInfo taskInfo); + void onTaskInfoChanged(RunningTaskInfo taskInfo); + void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo); + } + + private static class TaskData { + final RunningTaskInfo taskInfo; + final Listener listener; + + TaskData(RunningTaskInfo info, Listener l) { + taskInfo = info; + listener = l; + } + } + + private final Handler mHandler; + private final ShellTaskOrganizer mTaskOrganizer; + private final ArrayMap<WindowContainerToken, TaskData> mTasks = new ArrayMap<>(); + + private MultiWindowTaskListener.Listener mPendingListener; + + /** + * Create a listener for tasks in multi-window mode. + */ + public MultiWindowTaskListener(Handler handler, ShellTaskOrganizer organizer) { + mHandler = handler; + mTaskOrganizer = organizer; + mTaskOrganizer.addListener(this, TASK_LISTENER_TYPE_MULTI_WINDOW); + } + + /** + * @return the task organizer that is listened to. + */ + public TaskOrganizer getTaskOrganizer() { + return mTaskOrganizer; + } + + // TODO(b/129067201): track launches for bubbles + // Once we have key in ActivityOptions, match listeners via that key + public void setPendingListener(Listener listener) { + mPendingListener = listener; + } + + /** + * Removes a task listener previously registered when starting a new activity. + */ + public void removeListener(Listener listener) { + if (DEBUG) { + Log.d(TAG, "removeListener: listener=" + listener); + } + if (mPendingListener == listener) { + mPendingListener = null; + } + for (int i = 0; i < mTasks.size(); i++) { + if (mTasks.valueAt(i).listener == listener) { + mTasks.removeAt(i); + } + } + } + + @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + if (DEBUG) { + Log.d(TAG, "onTaskAppeared: taskInfo=" + taskInfo + + " mPendingListener=" + mPendingListener); + } + if (mPendingListener == null) { + // If there is no pending listener, then we are either receiving this task as a part of + // registering the task org again (ie. after SysUI dies) or the previously started + // task is no longer needed (ie. bubble is closed soon after), for now, just finish the + // associated task + try { + ActivityTaskManager.getService().removeTask(taskInfo.taskId); + } catch (RemoteException e) { + Log.w(TAG, "Failed to remove taskId " + taskInfo.taskId); + } + return; + } + + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(taskInfo.token, true); + + final TaskData data = new TaskData(taskInfo, mPendingListener); + mTasks.put(taskInfo.token, data); + mHandler.post(() -> data.listener.onTaskAppeared(taskInfo, leash)); + mPendingListener = null; + } + + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + final TaskData data = mTasks.remove(taskInfo.token); + if (data == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "onTaskVanished: taskInfo=" + taskInfo + " listener=" + data.listener); + } + mHandler.post(() -> data.listener.onTaskVanished(taskInfo)); + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final TaskData data = mTasks.get(taskInfo.token); + if (data == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "onTaskInfoChanged: taskInfo=" + taskInfo + " listener=" + data.listener); + } + mHandler.post(() -> data.listener.onTaskInfoChanged(taskInfo)); + } + + @Override + public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) { + final TaskData data = mTasks.get(taskInfo.token); + if (data == null) { + return; + } + + if (DEBUG) { + Log.d(TAG, "onTaskInfoChanged: taskInfo=" + taskInfo + " listener=" + data.listener); + } + mHandler.post(() -> data.listener.onBackPressedOnTaskRoot(taskInfo)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/ObjectWrapper.java b/packages/SystemUI/src/com/android/systemui/bubbles/ObjectWrapper.java new file mode 100644 index 000000000000..f054122eaa47 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/ObjectWrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2020 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.bubbles; + +import android.os.Binder; +import android.os.IBinder; + +// Copied from Launcher3 +/** + * Utility class to pass non-parcealable objects within same process using parcealable payload. + * + * It wraps the object in a binder as binders are singleton within a process + */ +public class ObjectWrapper<T> extends Binder { + + private T mObject; + + public ObjectWrapper(T object) { + mObject = object; + } + + public T get() { + return mObject; + } + + public void clear() { + mObject = null; + } + + public static IBinder wrap(Object obj) { + return new ObjectWrapper<>(obj); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt b/packages/SystemUI/src/com/android/systemui/bubbles/RelativeTouchListener.kt index 8880df9959c1..b1291a507b57 100644 --- a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/RelativeTouchListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.util +package com.android.systemui.bubbles import android.graphics.PointF import android.os.Handler diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/StackEducationView.kt b/packages/SystemUI/src/com/android/systemui/bubbles/StackEducationView.kt index 3e4c729d8315..216df2e1f402 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/StackEducationView.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/StackEducationView.kt @@ -24,21 +24,19 @@ import android.widget.LinearLayout import android.widget.TextView import com.android.internal.util.ContrastColorUtil import com.android.systemui.Interpolators -import com.android.systemui.Prefs -import com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION import com.android.systemui.R /** * User education view to highlight the collapsed stack of bubbles. * Shown only the first time a user taps the stack. */ -class StackEducationView constructor(context: Context) : LinearLayout(context){ +class StackEducationView constructor(context: Context) : LinearLayout(context) { private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" else BubbleDebugConfig.TAG_BUBBLES - private val ANIMATE_DURATION : Long = 200 - private val ANIMATE_DURATION_SHORT : Long = 40 + private val ANIMATE_DURATION: Long = 200 + private val ANIMATE_DURATION_SHORT: Long = 40 private val view by lazy { findViewById<View>(R.id.stack_education_layout) } private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) } @@ -47,7 +45,7 @@ class StackEducationView constructor(context: Context) : LinearLayout(context){ private var isHiding = false init { - LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this); + LayoutInflater.from(context).inflate(R.layout.bubble_stack_user_education, this) visibility = View.GONE elevation = resources.getDimensionPixelSize(R.dimen.bubble_elevation).toFloat() @@ -93,7 +91,7 @@ class StackEducationView constructor(context: Context) : LinearLayout(context){ * * @return true if user education was shown, false otherwise. */ - fun show(stackPosition: PointF) : Boolean{ + fun show(stackPosition: PointF): Boolean { if (visibility == VISIBLE) return false setAlpha(0f) @@ -129,6 +127,9 @@ class StackEducationView constructor(context: Context) : LinearLayout(context){ } private fun setShouldShow(shouldShow: Boolean) { - Prefs.putBoolean(context, HAS_SEEN_BUBBLES_EDUCATION, !shouldShow) + context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit().putBoolean(PREF_STACK_EDUCATION, !shouldShow).apply() } -}
\ No newline at end of file +} + +const val PREF_STACK_EDUCATION: String = "HasSeenBubblesOnboarding"
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/TaskView.java b/packages/SystemUI/src/com/android/systemui/bubbles/TaskView.java new file mode 100644 index 000000000000..524fa42af7d5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/TaskView.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2020 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.bubbles; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import dalvik.system.CloseGuard; + +/** + * View that can display a task. + */ +// TODO: Place in com.android.wm.shell vs. com.android.wm.shell.bubbles on shell migration. +public class TaskView extends SurfaceView implements SurfaceHolder.Callback, + MultiWindowTaskListener.Listener { + + public interface Listener { + /** Called when the container is ready for launching activities. */ + default void onInitialized() {} + + /** Called when the container can no longer launch activities. */ + default void onReleased() {} + + /** Called when a task is created inside the container. */ + default void onTaskCreated(int taskId, ComponentName name) {} + + /** Called when a task visibility changes. */ + default void onTaskVisibilityChanged(int taskId, boolean visible) {} + + /** Called when a task is about to be removed from the stack inside the container. */ + default void onTaskRemovalStarted(int taskId) {} + + /** Called when a task is created inside the container. */ + default void onBackPressedOnTaskRoot(int taskId) {} + } + + private final CloseGuard mGuard = CloseGuard.get(); + + private final MultiWindowTaskListener mMultiWindowTaskListener; + + private ActivityManager.RunningTaskInfo mTaskInfo; + private WindowContainerToken mTaskToken; + private SurfaceControl mTaskLeash; + private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + private boolean mSurfaceCreated; + private boolean mIsInitialized; + private Listener mListener; + + private final Rect mTmpRect = new Rect(); + private final Rect mTmpRootRect = new Rect(); + + public TaskView(Context context, MultiWindowTaskListener taskListener) { + super(context, null, 0, 0, true /* disableBackgroundLayer */); + + mMultiWindowTaskListener = taskListener; + setUseAlpha(); + getHolder().addCallback(this); + mGuard.open("release"); + } + + /** + * Only one listener may be set on the view, throws an exception otherwise. + */ + public void setListener(Listener listener) { + if (mListener != null) { + throw new IllegalStateException( + "Trying to set a listener when one has already been set"); + } + mListener = listener; + } + + /** + * Launch an activity represented by {@link ShortcutInfo}. + * <p>The owner of this container must be allowed to access the shortcut information, + * as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method. + * + * @param shortcut the shortcut used to launch the activity. + * @param options options for the activity. + * @param sourceBounds the rect containing the source bounds of the clicked icon to open + * this shortcut. + */ + public void startShortcutActivity(@NonNull ShortcutInfo shortcut, + @NonNull ActivityOptions options, @Nullable Rect sourceBounds) { + mMultiWindowTaskListener.setPendingListener(this); + prepareActivityOptions(options); + LauncherApps service = mContext.getSystemService(LauncherApps.class); + try { + service.startShortcut(shortcut, sourceBounds, options.toBundle()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Launch a new activity. + * + * @param pendingIntent Intent used to launch an activity. + * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()} + * @param options options for the activity. + */ + public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent, + @NonNull ActivityOptions options) { + mMultiWindowTaskListener.setPendingListener(this); + prepareActivityOptions(options); + try { + pendingIntent.send(mContext, 0 /* code */, fillInIntent, + null /* onFinished */, null /* handler */, null /* requiredPermission */, + options.toBundle()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void prepareActivityOptions(ActivityOptions options) { + options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + options.setTaskAlwaysOnTop(true); + } + + /** + * Call when view position or size has changed. Do not call when animating. + */ + public void onLocationChanged() { + if (mTaskToken == null) { + return; + } + // Update based on the screen bounds + getBoundsOnScreen(mTmpRect); + getRootView().getBoundsOnScreen(mTmpRootRect); + if (!mTmpRootRect.contains(mTmpRect)) { + mTmpRect.offsetTo(0, 0); + } + + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mTaskToken, mTmpRect); + // TODO(b/151449487): Enable synchronization + mMultiWindowTaskListener.getTaskOrganizer().applyTransaction(wct); + } + + /** + * Release this container if it is initialized. + */ + public void release() { + performRelease(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGuard != null) { + mGuard.warnIfOpen(); + performRelease(); + } + } finally { + super.finalize(); + } + } + + private void performRelease() { + getHolder().removeCallback(this); + mMultiWindowTaskListener.removeListener(this); + resetTaskInfo(); + mGuard.close(); + if (mListener != null && mIsInitialized) { + mListener.onReleased(); + mIsInitialized = false; + } + } + + private void resetTaskInfo() { + mTaskInfo = null; + mTaskToken = null; + mTaskLeash = null; + } + + private void updateTaskVisibility() { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */); + mMultiWindowTaskListener.getTaskOrganizer().applyTransaction(wct); + // TODO(b/151449487): Only call callback once we enable synchronization + if (mListener != null) { + mListener.onTaskVisibilityChanged(mTaskInfo.taskId, mSurfaceCreated); + } + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl leash) { + mTaskInfo = taskInfo; + mTaskToken = taskInfo.token; + mTaskLeash = leash; + + if (mSurfaceCreated) { + // Surface is ready, so just reparent the task to this surface control + mTransaction.reparent(mTaskLeash, getSurfaceControl()) + .show(mTaskLeash) + .apply(); + } else { + // The surface has already been destroyed before the task has appeared, so go ahead and + // hide the task entirely + updateTaskVisibility(); + } + + // TODO: Synchronize show with the resize + onLocationChanged(); + setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); + + if (mListener != null) { + mListener.onTaskCreated(taskInfo.taskId, taskInfo.baseActivity); + } + } + + @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (mTaskToken != null && mTaskToken.equals(taskInfo.token)) { + if (mListener != null) { + mListener.onTaskRemovalStarted(taskInfo.taskId); + } + + // Unparent the task when this surface is destroyed + mTransaction.reparent(mTaskLeash, null).apply(); + resetTaskInfo(); + } + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + mTaskInfo.taskDescription = taskInfo.taskDescription; + setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); + } + + @Override + public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { + if (mTaskToken != null && mTaskToken.equals(taskInfo.token)) { + if (mListener != null) { + mListener.onBackPressedOnTaskRoot(taskInfo.taskId); + } + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurfaceCreated = true; + if (mListener != null && !mIsInitialized) { + mIsInitialized = true; + mListener.onInitialized(); + } + if (mTaskToken == null) { + // Nothing to update, task is not yet available + return; + } + // Reparent the task when this surface is created + mTransaction.reparent(mTaskLeash, getSurfaceControl()) + .show(mTaskLeash) + .apply(); + updateTaskVisibility(); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + if (mTaskToken == null) { + return; + } + onLocationChanged(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurfaceCreated = false; + if (mTaskToken == null) { + // Nothing to update, task is not yet available + return; + } + + // Unparent the task when this surface is destroyed + mTransaction.reparent(mTaskLeash, null).apply(); + updateTaskVisibility(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java index f2a4f159f959..7fdc01961aa5 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java @@ -32,8 +32,8 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.systemui.Interpolators; import com.android.systemui.R; -import com.android.systemui.util.animation.PhysicsAnimator; -import com.android.systemui.util.magnetictarget.MagnetizedObject; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.google.android.collect.Sets; @@ -58,6 +58,9 @@ public class ExpandedAnimationController /** Duration of the expand/collapse target path animation. */ public static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175; + /** Damping ratio for expand/collapse spring. */ + private static final float DAMPING_RATIO_MEDIUM_LOW_BOUNCY = 0.65f; + /** Stiffness for the expand/collapse path-following animation. */ private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000; @@ -271,16 +274,14 @@ public class ExpandedAnimationController // Then, draw a line across the screen to the bubble's resting position. path.lineTo(getBubbleLeft(index), expandedY); } else { - final float sideMultiplier = - mLayout.isFirstChildXLeftOfCenter(mCollapsePoint.x) ? -1 : 1; - final float stackedX = mCollapsePoint.x + (sideMultiplier * index * mStackOffsetPx); + final float stackedX = mCollapsePoint.x; // If we're collapsing, draw a line from the bubble's current position to the side // of the screen where the bubble will be stacked. path.lineTo(stackedX, expandedY); // Then, draw a line down to the stack position. - path.lineTo(stackedX, mCollapsePoint.y); + path.lineTo(stackedX, mCollapsePoint.y + index * mStackOffsetPx); } // The lead bubble should be the bubble with the longest distance to travel when we're @@ -510,7 +511,7 @@ public class ExpandedAnimationController @Override SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { return new SpringForce() - .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + .setDampingRatio(DAMPING_RATIO_MEDIUM_LOW_BOUNCY) .setStiffness(SpringForce.STIFFNESS_LOW); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index e835ea206e59..12051241f049 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java @@ -36,9 +36,9 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.systemui.R; import com.android.systemui.bubbles.BubbleStackView; -import com.android.systemui.util.FloatingContentCoordinator; -import com.android.systemui.util.animation.PhysicsAnimator; -import com.android.systemui.util.magnetictarget.MagnetizedObject; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.google.android.collect.Sets; @@ -72,9 +72,9 @@ public class StackAnimationController extends /** * Values to use for the default {@link SpringForce} provided to the physics animation layout. */ - public static final int DEFAULT_STIFFNESS = 12000; + public static final int SPRING_TO_TOUCH_STIFFNESS = 12000; public static final float IME_ANIMATION_STIFFNESS = SpringForce.STIFFNESS_LOW; - private static final int FLING_FOLLOW_STIFFNESS = 20000; + private static final int CHAIN_STIFFNESS = 600; public static final float DEFAULT_BOUNCINESS = 0.9f; private final PhysicsAnimator.SpringConfig mAnimateOutSpringConfig = @@ -629,7 +629,7 @@ public class StackAnimationController extends public void moveStackFromTouch(float x, float y) { // Begin the spring-to-touch catch up animation if needed. if (mSpringToTouchOnNextMotionEvent) { - springStack(x, y, DEFAULT_STIFFNESS); + springStack(x, y, SPRING_TO_TOUCH_STIFFNESS); mSpringToTouchOnNextMotionEvent = false; mFirstBubbleSpringingToTouch = true; } else if (mFirstBubbleSpringingToTouch) { @@ -744,15 +744,13 @@ public class StackAnimationController extends @Override float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { - if (property.equals(DynamicAnimation.TRANSLATION_X)) { + if (property.equals(DynamicAnimation.TRANSLATION_Y)) { // If we're in the dismiss target, have the bubbles pile on top of each other with no // offset. if (isStackStuckToTarget()) { return 0f; } else { - // Offset to the left if we're on the left, or the right otherwise. - return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x) - ? -mStackOffset : mStackOffset; + return mStackOffset; } } else { return 0f; @@ -762,14 +760,12 @@ public class StackAnimationController extends @Override SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { final ContentResolver contentResolver = mLayout.getContext().getContentResolver(); - final float stiffness = Settings.Secure.getFloat(contentResolver, "bubble_stiffness", - mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS /* default */); final float dampingRatio = Settings.Secure.getFloat(contentResolver, "bubble_damping", DEFAULT_BOUNCINESS); return new SpringForce() .setDampingRatio(dampingRatio) - .setStiffness(stiffness); + .setStiffness(CHAIN_STIFFNESS); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java index 9efc3c20f55a..6b5f237ac76f 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java @@ -19,13 +19,15 @@ package com.android.systemui.bubbles.dagger; import android.app.INotificationManager; import android.content.Context; import android.content.pm.LauncherApps; +import android.os.Handler; import android.view.WindowManager; +import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.bubbles.BubbleController; -import com.android.systemui.bubbles.BubbleData; -import com.android.systemui.bubbles.BubbleDataRepository; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -39,7 +41,9 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ZenModeController; -import com.android.systemui.util.FloatingContentCoordinator; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.common.FloatingContentCoordinator; import dagger.Module; import dagger.Provides; @@ -52,12 +56,11 @@ public interface BubbleModule { */ @SysUISingleton @Provides - static BubbleController newBubbleController( + static Bubbles newBubbleController( Context context, NotificationShadeWindowController notificationShadeWindowController, StatusBarStateController statusBarStateController, ShadeController shadeController, - BubbleData data, ConfigurationController configurationController, NotificationInterruptStateProvider interruptionStateProvider, ZenModeController zenModeController, @@ -68,18 +71,20 @@ public interface BubbleModule { FeatureFlags featureFlags, DumpManager dumpManager, FloatingContentCoordinator floatingContentCoordinator, - BubbleDataRepository bubbleDataRepository, SysUiState sysUiState, INotificationManager notifManager, IStatusBarService statusBarService, WindowManager windowManager, - LauncherApps launcherApps) { - return new BubbleController( + WindowManagerShellWrapper windowManagerShellWrapper, + LauncherApps launcherApps, + UiEventLogger uiEventLogger, + @Main Handler mainHandler, + ShellTaskOrganizer organizer) { + return BubbleController.create( context, notificationShadeWindowController, statusBarStateController, shadeController, - data, null /* synchronizer */, configurationController, interruptionStateProvider, @@ -91,11 +96,14 @@ public interface BubbleModule { featureFlags, dumpManager, floatingContentCoordinator, - bubbleDataRepository, sysUiState, notifManager, statusBarService, windowManager, - launcherApps); + windowManagerShellWrapper, + launcherApps, + uiEventLogger, + mainHandler, + organizer); } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt index f4479653d12e..ce0786d86460 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubblePersistentRepository.kt @@ -18,16 +18,11 @@ package com.android.systemui.bubbles.storage import android.content.Context import android.util.AtomicFile import android.util.Log -import com.android.systemui.dagger.SysUISingleton import java.io.File import java.io.FileOutputStream import java.io.IOException -import javax.inject.Inject -@SysUISingleton -class BubblePersistentRepository @Inject constructor( - context: Context -) { +class BubblePersistentRepository(context: Context) { private val bubbleFile: AtomicFile = AtomicFile(File(context.filesDir, "overflow_bubbles.xml"), "overflow-bubbles") diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt index c6d57326357c..e0a7c7879f43 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleVolatileRepository.kt @@ -19,8 +19,6 @@ import android.content.pm.LauncherApps import android.os.UserHandle import com.android.internal.annotations.VisibleForTesting import com.android.systemui.bubbles.ShortcutKey -import com.android.systemui.dagger.SysUISingleton -import javax.inject.Inject private const val CAPACITY = 16 @@ -28,10 +26,7 @@ private const val CAPACITY = 16 * BubbleVolatileRepository holds the most updated snapshot of list of bubbles for in-memory * manipulation. */ -@SysUISingleton -class BubbleVolatileRepository @Inject constructor( - private val launcherApps: LauncherApps -) { +class BubbleVolatileRepository(private val launcherApps: LauncherApps) { /** * An ordered set of bubbles based on their natural ordering. */ diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt index 658f46e3bb96..2f0fd99337e5 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui.controls.controller -import android.app.ActivityManager import android.content.ComponentName import android.content.Context import android.os.IBinder @@ -30,6 +29,7 @@ import android.util.Log import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy import java.util.concurrent.atomic.AtomicBoolean @@ -40,7 +40,8 @@ import javax.inject.Inject open class ControlsBindingControllerImpl @Inject constructor( private val context: Context, @Background private val backgroundExecutor: DelayableExecutor, - private val lazyController: Lazy<ControlsController> + private val lazyController: Lazy<ControlsController>, + userTracker: UserTracker ) : ControlsBindingController { companion object { @@ -56,7 +57,7 @@ open class ControlsBindingControllerImpl @Inject constructor( } } - private var currentUser = UserHandle.of(ActivityManager.getCurrentUser()) + private var currentUser = userTracker.userHandle override val currentUserId: Int get() = currentUser.identifier diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 495872f3433d..d3d24be0ad9d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui.controls.controller -import android.app.ActivityManager import android.app.PendingIntent import android.app.backup.BackupManager import android.content.BroadcastReceiver @@ -46,6 +45,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.globalactions.GlobalActionsDialog +import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.DelayableExecutor import java.io.FileDescriptor import java.io.PrintWriter @@ -56,14 +56,15 @@ import javax.inject.Inject @SysUISingleton class ControlsControllerImpl @Inject constructor ( - private val context: Context, - @Background private val executor: DelayableExecutor, - private val uiController: ControlsUiController, - private val bindingController: ControlsBindingController, - private val listingController: ControlsListingController, - private val broadcastDispatcher: BroadcastDispatcher, - optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, - dumpManager: DumpManager + private val context: Context, + @Background private val executor: DelayableExecutor, + private val uiController: ControlsUiController, + private val bindingController: ControlsBindingController, + private val listingController: ControlsListingController, + private val broadcastDispatcher: BroadcastDispatcher, + optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, + dumpManager: DumpManager, + userTracker: UserTracker ) : Dumpable, ControlsController { companion object { @@ -85,7 +86,7 @@ class ControlsControllerImpl @Inject constructor ( private var seedingInProgress = false private val seedingCallbacks = mutableListOf<Consumer<Boolean>>() - private var currentUser = UserHandle.of(ActivityManager.getCurrentUser()) + private var currentUser = userTracker.userHandle override val currentUserId get() = currentUser.identifier diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt index 0d4439fe8ccb..2d76ff2774d6 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui.controls.management -import android.app.ActivityManager import android.content.ComponentName import android.content.Context import android.content.pm.ServiceInfo @@ -29,6 +28,7 @@ import com.android.settingslib.widget.CandidateInfo import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicInteger import javax.inject.Inject @@ -56,14 +56,16 @@ private fun createServiceListing(context: Context): ServiceListing { class ControlsListingControllerImpl @VisibleForTesting constructor( private val context: Context, @Background private val backgroundExecutor: Executor, - private val serviceListingBuilder: (Context) -> ServiceListing + private val serviceListingBuilder: (Context) -> ServiceListing, + userTracker: UserTracker ) : ControlsListingController { @Inject - constructor(context: Context, executor: Executor): this( + constructor(context: Context, executor: Executor, userTracker: UserTracker): this( context, executor, - ::createServiceListing + ::createServiceListing, + userTracker ) private var serviceListing = serviceListingBuilder(context) @@ -78,7 +80,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( private var availableServices = emptyList<ServiceInfo>() private var userChangeInProgress = AtomicInteger(0) - override var currentUserId = ActivityManager.getCurrentUser() + override var currentUserId = userTracker.userId private set private val serviceListingCallback = ServiceListing.Callback { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index 28bcf3a35117..d13e194a0d44 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -19,13 +19,13 @@ package com.android.systemui.dagger; import android.app.Activity; import com.android.systemui.ForegroundServicesDialog; -import com.android.systemui.bubbles.BubbleOverflowActivity; import com.android.systemui.keyguard.WorkLockActivity; import com.android.systemui.screenrecord.ScreenRecordDialog; import com.android.systemui.settings.BrightnessDialog; import com.android.systemui.tuner.TunerActivity; import com.android.systemui.usb.UsbDebuggingActivity; import com.android.systemui.usb.UsbDebuggingSecondaryUserActivity; +import com.android.systemui.user.CreateUserActivity; import dagger.Binds; import dagger.Module; @@ -67,12 +67,6 @@ public abstract class DefaultActivityBinder { @ClassKey(ScreenRecordDialog.class) public abstract Activity bindScreenRecordDialog(ScreenRecordDialog activity); - /** Inject into BubbleOverflowActivity. */ - @Binds - @IntoMap - @ClassKey(BubbleOverflowActivity.class) - public abstract Activity bindBubbleOverflowActivity(BubbleOverflowActivity activity); - /** Inject into UsbDebuggingActivity. */ @Binds @IntoMap @@ -85,4 +79,10 @@ public abstract class DefaultActivityBinder { @ClassKey(UsbDebuggingSecondaryUserActivity.class) public abstract Activity bindUsbDebuggingSecondaryUserActivity( UsbDebuggingSecondaryUserActivity activity); + + /** Inject into CreateUserActivity. */ + @Binds + @IntoMap + @ClassKey(CreateUserActivity.class) + public abstract Activity bindCreateUserActivity(CreateUserActivity activity); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java index eb431274b8a3..cb90b6114396 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyProvider.java @@ -66,6 +66,8 @@ import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.plugins.PluginManagerImpl; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.DevicePolicyManagerWrapper; +import com.android.systemui.shared.system.TaskStackChangeListeners; +import com.android.systemui.shared.system.WindowManagerWrapper; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.phone.AutoHideController; @@ -262,6 +264,13 @@ public class DependencyProvider { return ActivityManagerWrapper.getInstance(); } + /** */ + @Provides + @SysUISingleton + public TaskStackChangeListeners provideTaskStackChangeListeners() { + return TaskStackChangeListeners.getInstance(); + } + /** Provides and initializes the {#link BroadcastDispatcher} for SystemUI */ @Provides @SysUISingleton @@ -313,6 +322,13 @@ public class DependencyProvider { /** */ @Provides + public WindowManagerWrapper providesWindowManagerWrapper() { + return WindowManagerWrapper.getInstance(); + } + + /** */ + @Provides + @SysUISingleton public SystemActions providesSystemActions(Context context) { return new SystemActions(context); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 8f4e738e5a5f..63d9a831b33f 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -41,8 +41,10 @@ import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfC import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.dagger.StatusBarComponent; import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; import com.android.systemui.tuner.dagger.TunerModule; +import com.android.systemui.user.UserModule; import com.android.systemui.util.concurrency.SysUIConcurrencyModule; import com.android.systemui.util.dagger.UtilModule; import com.android.systemui.util.sensors.SensorModule; @@ -73,19 +75,23 @@ import dagger.Provides; SensorModule.class, SettingsModule.class, SettingsUtilModule.class, + SmartRepliesInflationModule.class, StatusBarPolicyModule.class, SysUIConcurrencyModule.class, TunerModule.class, + UserModule.class, UtilModule.class, VolumeModule.class }, - subcomponents = {StatusBarComponent.class, - NotificationRowComponent.class, - DozeComponent.class, - ExpandableNotificationRowComponent.class, - KeyguardBouncerComponent.class, - NotificationShelfComponent.class, - FragmentService.FragmentCreator.class}) + subcomponents = { + StatusBarComponent.class, + NotificationRowComponent.class, + DozeComponent.class, + ExpandableNotificationRowComponent.class, + KeyguardBouncerComponent.class, + NotificationShelfComponent.class, + FragmentService.FragmentCreator.class + }) public abstract class SystemUIModule { @Binds diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 424a8246b278..f470a6b55b76 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -123,6 +123,14 @@ public class DozeLog implements Dumpable { } /** + * Appends dozing event to the logs + * @param suppressed true if dozing is suppressed + */ + public void traceDozingSuppressed(boolean suppressed) { + mLogger.logDozingSuppressed(suppressed); + } + + /** * Appends fling event to the logs */ public void traceFling(boolean expand, boolean aboveThreshold, boolean thresholdNeeded, @@ -198,6 +206,22 @@ public class DozeLog implements Dumpable { } /** + * Appends doze state changed sent to all DozeMachine parts event to the logs + * @param state new DozeMachine state + */ + public void traceDozeStateSendComplete(DozeMachine.State state) { + mLogger.logStateChangedSent(state); + } + + /** + * Appends display state changed event to the logs + * @param displayState new DozeMachine state + */ + public void traceDisplayState(int displayState) { + mLogger.logDisplayStateChanged(displayState); + } + + /** * Appends wake-display event to the logs. * @param wake if we're waking up or sleeping. */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt index 732745a1158b..0c9e14352946 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt @@ -64,6 +64,14 @@ class DozeLogger @Inject constructor( }) } + fun logDozingSuppressed(isDozingSuppressed: Boolean) { + buffer.log(TAG, INFO, { + bool1 = isDozingSuppressed + }, { + "DozingSuppressed=$bool1" + }) + } + fun logFling( expand: Boolean, aboveThreshold: Boolean, @@ -143,6 +151,22 @@ class DozeLogger @Inject constructor( }) } + fun logStateChangedSent(state: DozeMachine.State) { + buffer.log(TAG, INFO, { + str1 = state.name + }, { + "Doze state sent to all DozeMachineParts stateSent=$str1" + }) + } + + fun logDisplayStateChanged(displayState: Int) { + buffer.log(TAG, INFO, { + int1 = displayState + }, { + "Display state changed to $int1" + }) + } + fun logWakeDisplay(isAwake: Boolean) { buffer.log(TAG, DEBUG, { bool1 = isAwake diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index d5820f3e05e4..befb648152d4 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -205,6 +205,7 @@ public class DozeMachine { } void onScreenState(int state) { + mDozeLog.traceDisplayState(state); for (Part part : mParts) { part.onScreenState(state); } @@ -308,6 +309,7 @@ public class DozeMachine { for (Part p : mParts) { p.transitionTo(oldState, newState); } + mDozeLog.traceDozeStateSendComplete(newState); switch (newState) { case FINISH: @@ -411,6 +413,7 @@ public class DozeMachine { pw.print(" state="); pw.println(mState); pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState); pw.print(" wakeLock="); pw.println(mWakeLock); + pw.print(" isDozeSuppressed="); pw.println(mDozeHost.isDozeSuppressed()); pw.println("Parts:"); for (Part p : mParts) { p.dump(pw); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java index 5aeb8df2028d..92494cf5b546 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java @@ -35,6 +35,7 @@ import com.android.systemui.doze.dagger.DozeScope; import com.android.systemui.doze.dagger.WrappedService; import com.android.systemui.util.sensors.AsyncSensorManager; +import java.io.PrintWriter; import java.util.Optional; import javax.inject.Inject; @@ -221,4 +222,9 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi mDebugBrightnessBucket = intent.getIntExtra(BRIGHTNESS_BUCKET, -1); updateBrightnessAndReady(false /* force */); } + + /** Dump current state */ + public void dump(PrintWriter pw) { + pw.println("DozeScreenBrightnessSensorRegistered=" + mRegistered); + } } diff --git a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeScope.java b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeScope.java index 7a8b8166a969..435859afc03f 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeScope.java +++ b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeScope.java @@ -24,7 +24,7 @@ import java.lang.annotation.Retention; import javax.inject.Scope; /** - * Scope annotation for singleton items within the StatusBarComponent. + * Scope annotation for singleton items within the DozeComponent. */ @Documented @Retention(RUNTIME) diff --git a/packages/SystemUI/src/com/android/systemui/emergency/EmergencyGesture.java b/packages/SystemUI/src/com/android/systemui/emergency/EmergencyGesture.java new file mode 100644 index 000000000000..dccb24dfe21e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/emergency/EmergencyGesture.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 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.emergency; + +/** + * Constants for the Emergency gesture. + * + * TODO (b/169175022) Update classname and docs when feature name is locked + */ +public final class EmergencyGesture { + + /** + * Launches the emergency flow. + * + * <p>The emergency flow is triggered by the Emergency gesture. By default the flow will call + * local emergency services, though OEMs can customize the flow. + * + * <p>This action can only be triggered by System UI through the emergency gesture. + * + * <p>TODO (b/169175022) Update action name and docs when feature name is locked + */ + public static final String ACTION_LAUNCH_EMERGENCY = + "com.android.systemui.action.LAUNCH_EMERGENCY"; + + private EmergencyGesture() {} +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index daef2c506ad3..5f726cd1e1f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -38,6 +38,7 @@ import android.provider.Settings; import android.service.notification.ZenModeConfig; import android.text.TextUtils; import android.text.style.StyleSpan; +import android.util.Log; import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; @@ -52,6 +53,8 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SystemUIAppComponentFactory; +import com.android.systemui.SystemUIFactory; +import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.StatusBarState; @@ -62,6 +65,8 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.util.wakelock.SettableWakeLock; import com.android.systemui.util.wakelock.WakeLock; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -80,6 +85,8 @@ public class KeyguardSliceProvider extends SliceProvider implements NotificationMediaManager.MediaListener, StatusBarStateController.StateListener, SystemUIAppComponentFactory.ContextInitializer { + private static final String TAG = "KgdSliceProvider"; + private static final StyleSpan BOLD_STYLE = new StyleSpan(Typeface.BOLD); public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main"; private static final String KEYGUARD_HEADER_URI = @@ -310,7 +317,25 @@ public class KeyguardSliceProvider extends SliceProvider implements mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern); mPendingIntent = PendingIntent.getActivity(getContext(), 0, new Intent(getContext(), KeyguardSliceProvider.class), 0); - mMediaManager.addCallback(this); + try { + //TODO(b/168778439): Remove this whole try catch. This is for debugging in dogfood. + mMediaManager.addCallback(this); + } catch (NullPointerException e) { + // We are sometimes failing to set the media manager. Why? + Log.w(TAG, "Failed to setup mMediaManager. Trying again."); + SysUIComponent rootComponent = SystemUIFactory.getInstance().getSysUIComponent(); + try { + Method injectMethod = rootComponent.getClass() + .getMethod("inject", getClass()); + injectMethod.invoke(rootComponent, this); + Log.w("TAG", "mMediaManager is now: " + mMediaManager); + } catch (NoSuchMethodException ex) { + Log.e(TAG, "Failed to find inject method for KeyguardSliceProvider", ex); + } catch (IllegalAccessException | InvocationTargetException ex) { + Log.e(TAG, "Failed to call inject", ex); + } + throw e; + } mStatusBarStateController.addCallback(this); mNextAlarmController.addCallback(this); mZenModeController.addCallback(this); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/Lifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/Lifecycle.java index 1b20cfbc4e55..3da6caf31968 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/Lifecycle.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/Lifecycle.java @@ -16,7 +16,10 @@ package com.android.systemui.keyguard; +import androidx.annotation.NonNull; + import java.util.ArrayList; +import java.util.Objects; import java.util.function.Consumer; /** @@ -26,8 +29,8 @@ public class Lifecycle<T> { private ArrayList<T> mObservers = new ArrayList<>(); - public void addObserver(T observer) { - mObservers.add(observer); + public void addObserver(@NonNull T observer) { + mObservers.add(Objects.requireNonNull(observer)); } public void removeObserver(T observer) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java index c281ece59c5a..75851102bc4a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java @@ -30,8 +30,8 @@ import android.os.UserHandle; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; public class WorkLockActivityController { private static final String TAG = WorkLockActivityController.class.getSimpleName(); @@ -40,16 +40,16 @@ public class WorkLockActivityController { private final IActivityTaskManager mIatm; public WorkLockActivityController(Context context) { - this(context, ActivityManagerWrapper.getInstance(), ActivityTaskManager.getService()); + this(context, TaskStackChangeListeners.getInstance(), ActivityTaskManager.getService()); } @VisibleForTesting WorkLockActivityController( - Context context, ActivityManagerWrapper am, IActivityTaskManager iAtm) { + Context context, TaskStackChangeListeners tscl, IActivityTaskManager iAtm) { mContext = context; mIatm = iAtm; - am.registerTaskStackListener(mLockListener); + tscl.registerTaskStackListener(mLockListener); } private void startWorkChallengeInTask(int taskId, int userId) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 9d8e73a0ff47..e50fd6a96b5c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -134,7 +134,7 @@ public class KeyguardModule { // Cameras that support "self illumination," via IR for example, don't need low light // environment mitigation. - boolean needsLowLightMitigation = faceManager.getSensorProperties().stream() + boolean needsLowLightMitigation = faceManager.getSensorPropertiesInternal().stream() .anyMatch((properties) -> !properties.supportsSelfIllumination); if (!needsLowLightMitigation) { return Optional.empty(); diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 6db408659c96..e3ee2a10821b 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -108,6 +108,18 @@ public class LogModule { return buffer; } + /** Provides a logging buffer for all logs related to Toasts shown by SystemUI. */ + @Provides + @SysUISingleton + @ToastLog + public static LogBuffer provideToastLogBuffer( + LogcatEchoTracker bufferFilter, + DumpManager dumpManager) { + LogBuffer buffer = new LogBuffer("ToastLog", 50, 10, bufferFilter); + buffer.attach(dumpManager); + return buffer; + } + /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java new file mode 100644 index 000000000000..8671dbfdf1fe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ToastLog.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020 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.log.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.android.systemui.log.LogBuffer; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** A {@link LogBuffer} for ToastLog-related messages. */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface ToastLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java new file mode 100644 index 000000000000..aca033e99623 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 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.media; + +import android.content.ComponentName; +import android.content.Context; +import android.media.browse.MediaBrowser; +import android.os.Bundle; + +import javax.inject.Inject; + +/** + * Testable wrapper around {@link MediaBrowser} constructor + */ +public class MediaBrowserFactory { + private final Context mContext; + + @Inject + public MediaBrowserFactory(Context context) { + mContext = context; + } + + /** + * Creates a new MediaBrowser + * + * @param serviceComponent + * @param callback + * @param rootHints + * @return + */ + public MediaBrowser create(ComponentName serviceComponent, + MediaBrowser.ConnectionCallback callback, Bundle rootHints) { + return new MediaBrowser(mContext, serviceComponent, callback, rootHints); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index f150381f4070..1beb875af70c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -174,7 +174,7 @@ class MediaCarouselController @Inject constructor( mediaManager.addListener(object : MediaDataManager.Listener { override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { addOrUpdatePlayer(key, oldKey, data) - val canRemove = data.isPlaying?.let { !it } ?: data.isClearable + val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active if (canRemove && !Utils.useMediaResumption(context)) { // This view isn't playing, let's remove this! This happens e.g when // dismissing/timing out a view. We still have the data around because @@ -250,13 +250,13 @@ class MediaCarouselController @Inject constructor( val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) newPlayer.view?.player?.setLayoutParams(lp) - newPlayer.bind(data) + newPlayer.bind(data, key) newPlayer.setListening(currentlyExpanded) MediaPlayerData.addMediaPlayer(key, data, newPlayer) updatePlayerToState(newPlayer, noAnimation = true) reorderAllPlayers() } else { - existingPlayer.bind(data) + existingPlayer.bind(data, key) MediaPlayerData.addMediaPlayer(key, data, existingPlayer) if (visualStabilityManager.isReorderingAllowed) { reorderAllPlayers() @@ -274,7 +274,7 @@ class MediaCarouselController @Inject constructor( } } - private fun removePlayer(key: String) { + private fun removePlayer(key: String, dismissMediaData: Boolean = true) { val removed = MediaPlayerData.removeMediaPlayer(key) removed?.apply { mediaCarouselScrollHandler.onPrePlayerRemoved(removed) @@ -283,13 +283,16 @@ class MediaCarouselController @Inject constructor( mediaCarouselScrollHandler.onPlayersChanged() updatePageIndicator() - // Inform the media manager of a potentially late dismissal - mediaManager.dismissMediaData(key, 0L) + if (dismissMediaData) { + // Inform the media manager of a potentially late dismissal + mediaManager.dismissMediaData(key, 0L) + } } } private fun recreatePlayers() { MediaPlayerData.mediaData().forEach { (key, data) -> + removePlayer(key, dismissMediaData = false) addOrUpdatePlayer(key = key, oldKey = null, data = data) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index 486399979db7..d80aafb714d3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -32,7 +32,7 @@ import com.android.systemui.qs.PageIndicator import com.android.systemui.R import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS import com.android.systemui.plugins.FalsingManager -import com.android.systemui.util.animation.PhysicsAnimator +import com.android.wm.shell.animation.PhysicsAnimator import com.android.systemui.util.concurrency.DelayableExecutor private const val FLING_SLOP = 1000000 diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index e55678dc986b..5b096ea363b6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -42,10 +42,10 @@ import androidx.annotation.UiThread; import androidx.constraintlayout.widget.ConstraintSet; import com.android.settingslib.Utils; -import com.android.settingslib.media.MediaOutputSliceConstants; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.util.animation.TransitionLayout; @@ -82,6 +82,7 @@ public class MediaControlPanel { private Context mContext; private PlayerViewHolder mViewHolder; + private String mKey; private MediaViewController mMediaViewController; private MediaSession.Token mToken; private MediaController mController; @@ -92,7 +93,7 @@ public class MediaControlPanel { private int mAlbumArtRadius; // This will provide the corners for the album art. private final ViewOutlineProvider mViewOutlineProvider; - + private final MediaOutputDialogFactory mMediaOutputDialogFactory; /** * Initialize a new control panel * @param context @@ -103,7 +104,8 @@ public class MediaControlPanel { public MediaControlPanel(Context context, @Background Executor backgroundExecutor, ActivityStarter activityStarter, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, - KeyguardDismissUtil keyguardDismissUtil) { + KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory + mediaOutputDialogFactory) { mContext = context; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; @@ -111,6 +113,7 @@ public class MediaControlPanel { mMediaViewController = mediaViewController; mMediaDataManagerLazy = lazyMediaDataManager; mKeyguardDismissUtil = keyguardDismissUtil; + mMediaOutputDialogFactory = mediaOutputDialogFactory; loadDimens(); mViewOutlineProvider = new ViewOutlineProvider() { @@ -206,10 +209,11 @@ public class MediaControlPanel { /** * Bind this view based on the data given */ - public void bind(@NonNull MediaData data) { + public void bind(@NonNull MediaData data, String key) { if (mViewHolder == null) { return; } + mKey = key; MediaSession.Token token = data.getToken(); mBackgroundColor = data.getBackgroundColor(); if (mToken == null || !mToken.equals(token)) { @@ -249,10 +253,9 @@ public class MediaControlPanel { // App icon ImageView appIcon = mViewHolder.getAppIcon(); if (data.getAppIcon() != null) { - appIcon.setImageDrawable(data.getAppIcon()); + appIcon.setImageIcon(data.getAppIcon()); } else { - Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note); - appIcon.setImageDrawable(iconDrawable); + appIcon.setImageResource(R.drawable.ic_music_note); } // Song name @@ -272,13 +275,7 @@ public class MediaControlPanel { setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); mViewHolder.getSeamless().setOnClickListener(v -> { - final Intent intent = new Intent() - .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) - .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, - data.getPackageName()) - .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken); - mActivityStarter.startActivity(intent, false, true /* dismissShade */, - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + mMediaOutputDialogFactory.create(data.getPackageName(), true); }); ImageView iconView = mViewHolder.getSeamlessIcon(); @@ -330,7 +327,7 @@ public class MediaControlPanel { int actionId = ACTION_IDS[i]; final ImageButton button = mViewHolder.getAction(actionId); MediaAction mediaAction = actionIcons.get(i); - button.setImageDrawable(mediaAction.getDrawable()); + button.setImageIcon(mediaAction.getIcon()); button.setContentDescription(mediaAction.getContentDescription()); Runnable action = mediaAction.getAction(); @@ -359,10 +356,10 @@ public class MediaControlPanel { // Dismiss mViewHolder.getDismiss().setOnClickListener(v -> { - if (data.getNotificationKey() != null) { + if (mKey != null) { closeGuts(); mKeyguardDismissUtil.executeWhenUnlocked(() -> { - mMediaDataManagerLazy.get().dismissMediaData(data.getNotificationKey(), + mMediaDataManagerLazy.get().dismissMediaData(mKey, MediaViewController.GUTS_ANIMATION_DURATION + 100); return true; }, /* requiresShadeOpen */ true); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 40a879abde34..0ed96eeac402 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -33,7 +33,7 @@ data class MediaData( /** * Icon shown on player, close to app name. */ - val appIcon: Drawable?, + val appIcon: Icon?, /** * Artist name. */ @@ -109,7 +109,7 @@ data class MediaData( /** State of a media action. */ data class MediaAction( - val drawable: Drawable?, + val icon: Icon?, val action: Runnable?, val contentDescription: CharSequence? ) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index cb6b22c2321f..6f6ee4c8091d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -455,7 +455,7 @@ class MediaDataManager( val app = builder.loadHeaderAppName() // App Icon - val smallIconDrawable: Drawable = sbn.notification.smallIcon.loadDrawable(context) + val smallIcon = sbn.notification.smallIcon // Song name var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) @@ -501,8 +501,13 @@ class MediaDataManager( } else { null } + val mediaActionIcon = if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { + Icon.createWithResource(packageContext, action.getIcon()!!.getResId()) + } else { + action.getIcon() + } val mediaAction = MediaAction( - action.getIcon().loadDrawable(packageContext), + mediaActionIcon, runnable, action.title) actionIcons.add(mediaAction) @@ -518,7 +523,7 @@ class MediaDataManager( val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true val active = mediaEntries[key]?.active ?: true onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app, - smallIconDrawable, artist, song, artWorkIcon, actionIcons, + smallIcon, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, active, resumeAction = resumeAction, isLocalSession = isLocalSession, notificationKey = key, hasCheckedForResume = hasCheckedForResume, @@ -572,7 +577,7 @@ class MediaDataManager( val source = ImageDecoder.createSource(context.getContentResolver(), uri) return try { ImageDecoder.decodeBitmap(source) { - decoder, info, source -> decoder.isMutableRequired = true + decoder, info, source -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE } } catch (e: IOException) { Log.e(TAG, "Unable to load bitmap", e) @@ -612,7 +617,7 @@ class MediaDataManager( private fun getResumeMediaAction(action: Runnable): MediaAction { return MediaAction( - context.getDrawable(R.drawable.lb_ic_play), + Icon.createWithResource(context, R.drawable.lb_ic_play), action, context.getString(R.string.controls_media_resume) ) @@ -631,13 +636,13 @@ class MediaDataManager( Assert.isMainThread() val removed = mediaEntries.remove(key) if (useMediaResumption && removed?.resumeAction != null && - !isBlockedFromResume(removed?.packageName)) { + !isBlockedFromResume(removed.packageName)) { Log.d(TAG, "Not removing $key because resumable") // Move to resume key (aka package name) if that key doesn't already exist. val resumeAction = getResumeMediaAction(removed.resumeAction!!) val updated = removed.copy(token = null, actions = listOf(resumeAction), actionsToShowInCompact = listOf(0), active = false, resumption = true) - val pkg = removed?.packageName + val pkg = removed.packageName val migrate = mediaEntries.put(pkg, updated) == null // Notify listeners of "new" controls when migrating or removed and update when not if (migrate) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt index 2bc908be055c..a993d00df01e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt @@ -16,7 +16,6 @@ package com.android.systemui.media -import android.content.Context import android.media.MediaRouter2Manager import android.media.session.MediaController import androidx.annotation.AnyThread @@ -25,7 +24,6 @@ import androidx.annotation.WorkerThread import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.systemui.Dumpable -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager @@ -34,11 +32,13 @@ import java.io.PrintWriter import java.util.concurrent.Executor import javax.inject.Inject +private const val PLAYBACK_TYPE_UNKNOWN = 0 + /** * Provides information about the route (ie. device) where playback is occurring. */ class MediaDeviceManager @Inject constructor( - private val context: Context, + private val controllerFactory: MediaControllerFactory, private val localMediaManagerFactory: LocalMediaManagerFactory, private val mr2manager: MediaRouter2Manager, @Main private val fgExecutor: Executor, @@ -72,7 +72,7 @@ class MediaDeviceManager @Inject constructor( if (entry == null || entry?.token != data.token) { entry?.stop() val controller = data.token?.let { - MediaController(context, it) + controllerFactory.create(it) } entry = Entry(key, oldKey, controller, localMediaManagerFactory.create(data.packageName)) @@ -123,11 +123,12 @@ class MediaDeviceManager @Inject constructor( val oldKey: String?, val controller: MediaController?, val localMediaManager: LocalMediaManager - ) : LocalMediaManager.DeviceCallback { + ) : LocalMediaManager.DeviceCallback, MediaController.Callback() { val token get() = controller?.sessionToken private var started = false + private var playbackType = PLAYBACK_TYPE_UNKNOWN private var current: MediaDevice? = null set(value) { if (!started || value != field) { @@ -142,6 +143,8 @@ class MediaDeviceManager @Inject constructor( fun start() = bgExecutor.execute { localMediaManager.registerCallback(this) localMediaManager.startScan() + playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN + controller?.registerCallback(this) updateCurrent() started = true } @@ -149,22 +152,37 @@ class MediaDeviceManager @Inject constructor( @AnyThread fun stop() = bgExecutor.execute { started = false + controller?.unregisterCallback(this) localMediaManager.stopScan() localMediaManager.unregisterCallback(this) } fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { - val route = controller?.let { + val routingSession = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } + val selectedRoutes = routingSession?.let { + mr2manager.getSelectedRoutes(it) + } with(pw) { println(" current device is ${current?.name}") val type = controller?.playbackInfo?.playbackType - println(" PlaybackType=$type (1 for local, 2 for remote)") - println(" route=$route") + println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") + println(" routingSession=$routingSession") + println(" selectedRoutes=$selectedRoutes") } } + @WorkerThread + override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { + val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN + if (newPlaybackType == playbackType) { + return + } + playbackType = newPlaybackType + updateCurrent() + } + override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { updateCurrent() } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index 5b59214afdc9..5c1c60c5b07e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -28,6 +28,7 @@ import android.os.UserHandle import android.provider.Settings import android.service.media.MediaBrowserService import android.util.Log +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -47,7 +48,8 @@ class MediaResumeListener @Inject constructor( private val context: Context, private val broadcastDispatcher: BroadcastDispatcher, @Background private val backgroundExecutor: Executor, - private val tunerService: TunerService + private val tunerService: TunerService, + private val mediaBrowserFactory: ResumeMediaBrowserFactory ) : MediaDataManager.Listener { private var useMediaResumption: Boolean = Utils.useMediaResumption(context) @@ -59,7 +61,8 @@ class MediaResumeListener @Inject constructor( private var mediaBrowser: ResumeMediaBrowser? = null private var currentUserId: Int = context.userId - private val userChangeReceiver = object : BroadcastReceiver() { + @VisibleForTesting + val userChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_USER_UNLOCKED == intent.action) { loadMediaResumptionControls() @@ -152,7 +155,7 @@ class MediaResumeListener @Inject constructor( resumeComponents.forEach { if (!blockedApps.contains(it.packageName)) { - val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it) + val browser = mediaBrowserFactory.create(mediaBrowserCallback, it) browser.findRecentMedia() } } @@ -193,14 +196,10 @@ class MediaResumeListener @Inject constructor( private fun tryUpdateResumptionList(key: String, componentName: ComponentName) { Log.d(TAG, "Testing if we can connect to $componentName") mediaBrowser?.disconnect() - mediaBrowser = ResumeMediaBrowser(context, + mediaBrowser = mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { override fun onConnected() { - Log.d(TAG, "yes we can resume with $componentName") - mediaDataManager.setResumeAction(key, getResumeAction(componentName)) - updateResumptionList(componentName) - mediaBrowser?.disconnect() - mediaBrowser = null + Log.d(TAG, "Connected to $componentName") } override fun onError() { @@ -209,6 +208,19 @@ class MediaResumeListener @Inject constructor( mediaBrowser?.disconnect() mediaBrowser = null } + + override fun addTrack( + desc: MediaDescription, + component: ComponentName, + browser: ResumeMediaBrowser + ) { + // Since this is a test, just save the component for later + Log.d(TAG, "Can get resumable media from $componentName") + mediaDataManager.setResumeAction(key, getResumeAction(componentName)) + updateResumptionList(componentName) + mediaBrowser?.disconnect() + mediaBrowser = null + } }, componentName) mediaBrowser?.testConnection() @@ -245,7 +257,7 @@ class MediaResumeListener @Inject constructor( private fun getResumeAction(componentName: ComponentName): Runnable { return Runnable { mediaBrowser?.disconnect() - mediaBrowser = ResumeMediaBrowser(context, + mediaBrowser = mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { override fun onConnected() { if (mediaBrowser?.token == null) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt index b8872250bb6c..00273bc34552 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt @@ -8,7 +8,7 @@ import android.view.MotionEvent import android.view.ViewGroup import android.widget.HorizontalScrollView import com.android.systemui.Gefingerpoken -import com.android.systemui.util.animation.physicsAnimator +import com.android.wm.shell.animation.physicsAnimator /** * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index 6bd5274fa331..51dbfa733541 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -126,6 +126,7 @@ class MediaTimeoutListener @Inject constructor( fun destroy() { mediaController?.unregisterCallback(this) + cancellation?.run() } override fun onPlaybackStateChanged(state: PlaybackState?) { @@ -182,4 +183,4 @@ class MediaTimeoutListener @Inject constructor( cancellation = null } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt index 11551aca80f2..666a6038a8b6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt @@ -23,7 +23,6 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.SeekBar import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.R import com.android.systemui.util.animation.TransitionLayout diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java index 68b6785849aa..a4d44367be73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java @@ -30,6 +30,8 @@ import android.service.media.MediaBrowserService; import android.text.TextUtils; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + import java.util.List; /** @@ -46,6 +48,7 @@ public class ResumeMediaBrowser { private static final String TAG = "ResumeMediaBrowser"; private final Context mContext; private final Callback mCallback; + private MediaBrowserFactory mBrowserFactory; private MediaBrowser mMediaBrowser; private ComponentName mComponentName; @@ -55,10 +58,12 @@ public class ResumeMediaBrowser { * @param callback used to report media items found * @param componentName Component name of the MediaBrowserService this browser will connect to */ - public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) { + public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName, + MediaBrowserFactory browserFactory) { mContext = context; mCallback = callback; mComponentName = componentName; + mBrowserFactory = browserFactory; } /** @@ -74,7 +79,7 @@ public class ResumeMediaBrowser { disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, + mMediaBrowser = mBrowserFactory.create( mComponentName, mConnectionCallback, rootHints); @@ -88,17 +93,19 @@ public class ResumeMediaBrowser { List<MediaBrowser.MediaItem> children) { if (children.size() == 0) { Log.d(TAG, "No children found for " + mComponentName); - return; - } - // We ask apps to return a playable item as the first child when sending - // a request with EXTRA_RECENT; if they don't, no resume controls - MediaBrowser.MediaItem child = children.get(0); - MediaDescription desc = child.getDescription(); - if (child.isPlayable() && mMediaBrowser != null) { - mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), - ResumeMediaBrowser.this); + mCallback.onError(); } else { - Log.d(TAG, "Child found but not playable for " + mComponentName); + // We ask apps to return a playable item as the first child when sending + // a request with EXTRA_RECENT; if they don't, no resume controls + MediaBrowser.MediaItem child = children.get(0); + MediaDescription desc = child.getDescription(); + if (child.isPlayable() && mMediaBrowser != null) { + mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), + ResumeMediaBrowser.this); + } else { + Log.d(TAG, "Child found but not playable for " + mComponentName); + mCallback.onError(); + } } disconnect(); } @@ -131,7 +138,7 @@ public class ResumeMediaBrowser { Log.d(TAG, "Service connected for " + mComponentName); if (mMediaBrowser != null && mMediaBrowser.isConnected()) { String root = mMediaBrowser.getRoot(); - if (!TextUtils.isEmpty(root)) { + if (!TextUtils.isEmpty(root) && mMediaBrowser != null) { mCallback.onConnected(); mMediaBrowser.subscribe(root, mSubscriptionCallback); return; @@ -182,7 +189,7 @@ public class ResumeMediaBrowser { disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, mComponentName, + mMediaBrowser = mBrowserFactory.create(mComponentName, new MediaBrowser.ConnectionCallback() { @Override public void onConnected() { @@ -192,7 +199,7 @@ public class ResumeMediaBrowser { return; } MediaSession.Token token = mMediaBrowser.getSessionToken(); - MediaController controller = new MediaController(mContext, token); + MediaController controller = createMediaController(token); controller.getTransportControls(); controller.getTransportControls().prepare(); controller.getTransportControls().play(); @@ -212,6 +219,11 @@ public class ResumeMediaBrowser { mMediaBrowser.connect(); } + @VisibleForTesting + protected MediaController createMediaController(MediaSession.Token token) { + return new MediaController(mContext, token); + } + /** * Get the media session token * @return the token, or null if the MediaBrowser is null or disconnected @@ -235,42 +247,19 @@ public class ResumeMediaBrowser { /** * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser. - * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called - * depending on whether it was successful. + * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is + * found, then ResumeMediaBrowser.Callback#addTrack will also be called. This allows for more + * detailed logging if the service has issues. If it cannot connect, or cannot find valid media, + * then ResumeMediaBrowser.Callback#onError will be called. * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. */ public void testConnection() { disconnect(); - final MediaBrowser.ConnectionCallback connectionCallback = - new MediaBrowser.ConnectionCallback() { - @Override - public void onConnected() { - Log.d(TAG, "connected"); - if (mMediaBrowser == null || !mMediaBrowser.isConnected() - || TextUtils.isEmpty(mMediaBrowser.getRoot())) { - mCallback.onError(); - } else { - mCallback.onConnected(); - } - } - - @Override - public void onConnectionSuspended() { - Log.d(TAG, "suspended"); - mCallback.onError(); - } - - @Override - public void onConnectionFailed() { - Log.d(TAG, "failed"); - mCallback.onError(); - } - }; Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, + mMediaBrowser = mBrowserFactory.create( mComponentName, - connectionCallback, + mConnectionCallback, rootHints); mMediaBrowser.connect(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java new file mode 100644 index 000000000000..2261aa5ac265 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 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.media; + +import android.content.ComponentName; +import android.content.Context; + +import javax.inject.Inject; + +/** + * Testable wrapper around {@link ResumeMediaBrowser} constructor + */ +public class ResumeMediaBrowserFactory { + private final Context mContext; + private final MediaBrowserFactory mBrowserFactory; + + @Inject + public ResumeMediaBrowserFactory(Context context, MediaBrowserFactory browserFactory) { + mContext = context; + mBrowserFactory = browserFactory; + } + + /** + * Creates a new ResumeMediaBrowser. + * + * @param callback will be called on connection or error, and addTrack when media item found + * @param componentName component to browse + * @return + */ + public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback, + ComponentName componentName) { + return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index 9fc64d51cdf7..d1630ebe8dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -42,7 +42,9 @@ import java.util.List; public class MediaOutputAdapter extends MediaOutputBaseAdapter { private static final String TAG = "MediaOutputAdapter"; - private static final int PAIR_NEW = 1; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private ViewGroup mConnectedItem; public MediaOutputAdapter(MediaOutputController controller) { super(controller); @@ -58,11 +60,14 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { @Override public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) { - if (mController.isZeroMode() && position == (mController.getMediaDevices().size())) { - viewHolder.onBind(PAIR_NEW); - } else if (position < (mController.getMediaDevices().size())) { - viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position)); - } else { + final int size = mController.getMediaDevices().size(); + if (mController.isZeroMode() && position == size) { + viewHolder.onBind(CUSTOMIZED_ITEM_PAIR_NEW, false /* topMargin */, + true /* bottomMargin */); + } else if (position < size) { + viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position), + position == 0 /* topMargin */, position == (size - 1) /* bottomMargin */); + } else if (DEBUG) { Log.d(TAG, "Incorrect position: " + position); } } @@ -76,18 +81,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { return mController.getMediaDevices().size(); } - void onItemClick(MediaDevice device) { - mController.connectDevice(device); - device.setState(MediaDeviceState.STATE_CONNECTING); - notifyDataSetChanged(); - } - - void onItemClick(int customizedItem) { - if (customizedItem == PAIR_NEW) { - mController.launchBluetoothPairing(); - } - } - @Override CharSequence getItemTitle(MediaDevice device) { if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE @@ -112,51 +105,72 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } @Override - void onBind(MediaDevice device) { - super.onBind(device); + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { + super.onBind(device, topMargin, bottomMargin); + final boolean currentlyConnected = isCurrentlyConnected(device); + if (currentlyConnected) { + mConnectedItem = mFrameLayout; + } if (mController.isTransferring()) { if (device.getState() == MediaDeviceState.STATE_CONNECTING && !mController.hasAdjustVolumeUserRestriction()) { - setTwoLineLayout(device, true); - mProgressBar.setVisibility(View.VISIBLE); - mSeekBar.setVisibility(View.GONE); - mSubTitleText.setVisibility(View.GONE); + setTwoLineLayout(device, null /* title */, true /* bFocused */, + false /* showSeekBar*/, true /* showProgressBar */, + false /* showSubtitle */); } else { - setSingleLineLayout(getItemTitle(device), false); + setSingleLineLayout(getItemTitle(device), false /* bFocused */); } } else { // Set different layout for each device if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) { - setTwoLineLayout(device, false); - mSubTitleText.setVisibility(View.VISIBLE); - mSeekBar.setVisibility(View.GONE); - mProgressBar.setVisibility(View.GONE); + setTwoLineLayout(device, null /* title */, false /* bFocused */, + false /* showSeekBar*/, false /* showProgressBar */, + true /* showSubtitle */); mSubTitleText.setText(R.string.media_output_dialog_connect_failed); - mFrameLayout.setOnClickListener(v -> onItemClick(device)); + mFrameLayout.setOnClickListener(v -> onItemClick(v, device)); } else if (!mController.hasAdjustVolumeUserRestriction() - && isCurrentConnected(device)) { - setTwoLineLayout(device, true); - mSeekBar.setVisibility(View.VISIBLE); - mProgressBar.setVisibility(View.GONE); - mSubTitleText.setVisibility(View.GONE); + && currentlyConnected) { + setTwoLineLayout(device, null /* title */, true /* bFocused */, + true /* showSeekBar*/, false /* showProgressBar */, + false /* showSubtitle */); initSeekbar(device); } else { - setSingleLineLayout(getItemTitle(device), false); - mFrameLayout.setOnClickListener(v -> onItemClick(device)); + setSingleLineLayout(getItemTitle(device), false /* bFocused */); + mFrameLayout.setOnClickListener(v -> onItemClick(v, device)); } } } @Override - void onBind(int customizedItem) { - if (customizedItem == PAIR_NEW) { + void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) { + super.onBind(customizedItem, topMargin, bottomMargin); + if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) { setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new), - false); + false /* bFocused */); final Drawable d = mContext.getDrawable(R.drawable.ic_add); d.setColorFilter(new PorterDuffColorFilter( Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN)); mTitleIcon.setImageDrawable(d); - mFrameLayout.setOnClickListener(v -> onItemClick(PAIR_NEW)); + mFrameLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW)); + } + } + + private void onItemClick(View view, MediaDevice device) { + if (mController.isTransferring()) { + return; + } + + playSwitchingAnim(mConnectedItem, view); + mController.connectDevice(device); + device.setState(MediaDeviceState.STATE_CONNECTING); + if (!isAnimating()) { + notifyDataSetChanged(); + } + } + + private void onItemClick(int customizedItem) { + if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) { + mController.launchBluetoothPairing(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 7579c25b030a..2d3e77db1ea3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -16,6 +16,8 @@ package com.android.systemui.media.dialog; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.graphics.Typeface; import android.text.TextUtils; @@ -33,6 +35,7 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.android.settingslib.media.MediaDevice; +import com.android.systemui.Interpolators; import com.android.systemui.R; /** @@ -44,9 +47,13 @@ public abstract class MediaOutputBaseAdapter extends private static final String FONT_SELECTED_TITLE = "sans-serif-medium"; private static final String FONT_TITLE = "sans-serif"; + static final int CUSTOMIZED_ITEM_PAIR_NEW = 1; + final MediaOutputController mController; private boolean mIsDragging; + private int mMargin; + private boolean mIsAnimating; Context mContext; View mHolderView; @@ -60,6 +67,8 @@ public abstract class MediaOutputBaseAdapter extends public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { mContext = viewGroup.getContext(); + mMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_list_margin); mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item, viewGroup, false); @@ -70,7 +79,7 @@ public abstract class MediaOutputBaseAdapter extends return device.getName(); } - boolean isCurrentConnected(MediaDevice device) { + boolean isCurrentlyConnected(MediaDevice device) { return TextUtils.equals(device.getId(), mController.getCurrentConnectedMediaDevice().getId()); } @@ -79,10 +88,17 @@ public abstract class MediaOutputBaseAdapter extends return mIsDragging; } + boolean isAnimating() { + return mIsAnimating; + } + /** * ViewHolder for binding device view. */ abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder { + + private static final int ANIM_DURATION = 200; + final FrameLayout mFrameLayout; final TextView mTitleText; final TextView mTwoLineTitleText; @@ -106,15 +122,28 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar = view.requireViewById(R.id.volume_seekbar); } - void onBind(MediaDevice device) { + void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) { mTitleIcon.setImageIcon(mController.getDeviceIconCompat(device).toIcon(mContext)); + setMargin(topMargin, bottomMargin); + } + + void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) { + setMargin(topMargin, bottomMargin); } - void onBind(int customizedItem) { } + private void setMargin(boolean topMargin, boolean bottomMargin) { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mFrameLayout + .getLayoutParams(); + params.topMargin = topMargin ? mMargin : 0; + params.bottomMargin = bottomMargin ? mMargin : 0; + mFrameLayout.setLayoutParams(params); + } void setSingleLineLayout(CharSequence title, boolean bFocused) { - mTitleText.setVisibility(View.VISIBLE); mTwoLineLayout.setVisibility(View.GONE); + mProgressBar.setVisibility(View.GONE); + mTitleText.setVisibility(View.VISIBLE); + mTitleText.setTranslationY(0); mTitleText.setText(title); if (bFocused) { mTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL)); @@ -123,10 +152,21 @@ public abstract class MediaOutputBaseAdapter extends } } - void setTwoLineLayout(MediaDevice device, boolean bFocused) { + void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused, + boolean showSeekBar, boolean showProgressBar, boolean showSubtitle) { mTitleText.setVisibility(View.GONE); mTwoLineLayout.setVisibility(View.VISIBLE); - mTwoLineTitleText.setText(getItemTitle(device)); + mSeekBar.setAlpha(1); + mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE); + mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE); + mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE); + mTwoLineTitleText.setTranslationY(0); + if (device == null) { + mTwoLineTitleText.setText(title); + } else { + mTwoLineTitleText.setText(getItemTitle(device)); + } + if (bFocused) { mTwoLineTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL)); @@ -161,5 +201,53 @@ public abstract class MediaOutputBaseAdapter extends } }); } + + void playSwitchingAnim(@NonNull View from, @NonNull View to) { + final float delta = (float) (mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_title_anim_y_delta)); + final SeekBar fromSeekBar = from.requireViewById(R.id.volume_seekbar); + final TextView toTitleText = to.requireViewById(R.id.title); + if (fromSeekBar.getVisibility() != View.VISIBLE || toTitleText.getVisibility() + != View.VISIBLE) { + return; + } + mIsAnimating = true; + // Animation for title text + toTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL)); + toTitleText.animate() + .setDuration(ANIM_DURATION) + .translationY(-delta) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + to.requireViewById(R.id.volume_indeterminate_progress).setVisibility( + View.VISIBLE); + } + }); + // Animation for seek bar + fromSeekBar.animate() + .alpha(0) + .setDuration(ANIM_DURATION) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + final TextView fromTitleText = from.requireViewById( + R.id.two_line_title); + fromTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL)); + fromTitleText.animate() + .setDuration(ANIM_DURATION) + .translationY(delta) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mIsAnimating = false; + notifyDataSetChanged(); + } + }); + } + }); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index f8f4f4df58bc..caef536961f1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -33,7 +33,6 @@ import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; import android.widget.Button; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -50,7 +49,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog; * Base dialog for media output UI */ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements - MediaOutputController.Callback { + MediaOutputController.Callback, Window.Callback { private static final String TAG = "MediaOutputDialog"; @@ -69,12 +68,9 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements private LinearLayout mDeviceListLayout; private Button mDoneButton; private Button mStopButton; - private View mListBottomPadding; private int mListMaxHeight; MediaOutputBaseAdapter mAdapter; - FrameLayout mGroupItemController; - View mGroupDivider; private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> { // Set max height for list @@ -114,12 +110,9 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle); mHeaderIcon = mDialogView.requireViewById(R.id.header_icon); mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result); - mGroupItemController = mDialogView.requireViewById(R.id.group_item_controller); - mGroupDivider = mDialogView.requireViewById(R.id.group_item_divider); mDeviceListLayout = mDialogView.requireViewById(R.id.device_list); mDoneButton = mDialogView.requireViewById(R.id.done); mStopButton = mDialogView.requireViewById(R.id.stop); - mListBottomPadding = mDialogView.requireViewById(R.id.list_bottom_padding); mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener( mDeviceListLayoutListener); @@ -162,7 +155,9 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements } if (mHeaderIcon.getVisibility() == View.VISIBLE) { final int size = getHeaderIconSize(); - mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size, size)); + final int padding = mContext.getResources().getDimensionPixelSize( + R.dimen.media_output_dialog_header_icon_padding); + mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size)); } // Update title and subtitle mHeaderTitle.setText(getHeaderText()); @@ -175,15 +170,11 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements mHeaderSubtitle.setText(subTitle); mHeaderTitle.setGravity(Gravity.NO_GRAVITY); } - if (!mAdapter.isDragging()) { + if (!mAdapter.isDragging() && !mAdapter.isAnimating()) { mAdapter.notifyDataSetChanged(); } - // Add extra padding when device amount is less than 6 - if (mMediaOutputController.getMediaDevices().size() < 6) { - mListBottomPadding.setVisibility(View.VISIBLE); - } else { - mListBottomPadding.setVisibility(View.GONE); - } + // Show when remote media session is available + mStopButton.setVisibility(getStopButtonVisibility()); } abstract int getHeaderIconRes(); @@ -196,6 +187,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements abstract CharSequence getHeaderSubtitle(); + abstract int getStopButtonVisibility(); + @Override public void onMediaChanged() { mMainThreadHandler.post(() -> refresh()); @@ -217,4 +210,12 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements public void dismissDialog() { dismiss(); } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (!hasFocus && isShowing()) { + dismiss(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 64d20a273931..b1f1bda25961 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -24,6 +24,7 @@ import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.MediaMetadata; +import android.media.MediaRoute2Info; import android.media.RoutingSessionInfo; import android.media.session.MediaController; import android.media.session.MediaSessionManager; @@ -63,7 +64,7 @@ import javax.inject.Inject; public class MediaOutputController implements LocalMediaManager.DeviceCallback{ private static final String TAG = "MediaOutputController"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final String mPackageName; private final Context mContext; @@ -406,6 +407,14 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback{ mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true); } + boolean isActiveRemoteDevice(@NonNull MediaDevice device) { + final List<String> features = device.getFeatures(); + return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK) + || features.contains(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK) + || features.contains(MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK) + || features.contains(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK)); + } + private final MediaController.Callback mCb = new MediaController.Callback() { @Override public void onMetadataChanged(MediaMetadata metadata) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index ac9d8ce52d88..a892a12f387b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -45,8 +45,6 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mGroupItemController.setVisibility(View.GONE); - mGroupDivider.setVisibility(View.GONE); } @Override @@ -74,4 +72,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { CharSequence getHeaderSubtitle() { return mMediaOutputController.getHeaderSubTitle(); } + + @Override + int getStopButtonVisibility() { + return mMediaOutputController.isActiveRemoteDevice( + mMediaOutputController.getCurrentConnectedMediaDevice()) ? View.VISIBLE : View.GONE; + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt index bc1dca58990d..4cdca4cbcf1e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -33,10 +33,22 @@ class MediaOutputDialogFactory @Inject constructor( private val shadeController: ShadeController, private val starter: ActivityStarter ) { + companion object { + var mediaOutputDialog: MediaOutputDialog? = null + } + /** Creates a [MediaOutputDialog] for the given package. */ fun create(packageName: String, aboveStatusBar: Boolean) { - MediaOutputController(context, packageName, mediaSessionManager, lbm, shadeController, - starter).run { + mediaOutputDialog?.dismiss() + + mediaOutputDialog = MediaOutputController(context, packageName, mediaSessionManager, lbm, + shadeController, starter).run { MediaOutputDialog(context, aboveStatusBar, this) } } + + /** dismiss [MediaOutputDialog] if exist. */ + fun dismiss() { + mediaOutputDialog?.dismiss() + mediaOutputDialog = null + } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java index 6cbf065ea6a9..df9e7a428877 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java @@ -47,6 +47,7 @@ import com.android.systemui.R; import com.android.systemui.navigationbar.buttons.KeyButtonDrawable; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.RotationLockController; @@ -156,7 +157,7 @@ public class RotationButtonController { throw e.rethrowFromSystemServer(); } - ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); + TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); } void unregisterListeners() { @@ -171,7 +172,7 @@ public class RotationButtonController { throw e.rethrowFromSystemServer(); } - ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); + TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); } void addRotationCallback(Consumer<Integer> watcher) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonDrawable.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonDrawable.java index fc2016913292..702be72ff4ed 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonDrawable.java @@ -171,6 +171,24 @@ public class KeyButtonDrawable extends Drawable { } @Override + public boolean setVisible(boolean visible, boolean restart) { + boolean changed = super.setVisible(visible, restart); + if (changed) { + // End any existing animations when the visibility changes + jumpToCurrentState(); + } + return changed; + } + + @Override + public void jumpToCurrentState() { + super.jumpToCurrentState(); + if (mAnimatedDrawable != null) { + mAnimatedDrawable.jumpToCurrentState(); + } + } + + @Override public void setAlpha(int alpha) { mState.mAlpha = alpha; mIconPaint.setAlpha(alpha); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java index 72cd4f1343e6..cf45f52e3367 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java @@ -225,6 +225,16 @@ public class KeyButtonRipple extends Drawable { } @Override + public boolean setVisible(boolean visible, boolean restart) { + boolean changed = super.setVisible(visible, restart); + if (changed) { + // End any existing animations when the visibility changes + jumpToCurrentState(); + } + return changed; + } + + @Override public void jumpToCurrentState() { endAnimations("jumpToCurrentState", false /* cancel */); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java index d6b831640326..4efe4d85156b 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java @@ -58,7 +58,6 @@ import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.system.QuickStepContract; @@ -426,12 +425,6 @@ public class KeyButtonView extends ImageView implements ButtonInterface { if (getDisplay() != null) { displayId = getDisplay().getDisplayId(); } - // Bubble controller will give us a valid display id if it should get the back event - BubbleController bubbleController = Dependency.get(BubbleController.class); - int bubbleDisplayId = bubbleController.getExpandedDisplayId(mContext); - if (mCode == KeyEvent.KEYCODE_BACK && bubbleDisplayId != INVALID_DISPLAY) { - displayId = bubbleDisplayId; - } if (displayId != INVALID_DISPLAY) { ev.setDisplayId(displayId); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 56943604e9b3..18cc746666d8 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -15,8 +15,6 @@ */ package com.android.systemui.navigationbar.gestural; -import static android.view.Display.INVALID_DISPLAY; - import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; @@ -57,7 +55,6 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SystemUIFactory; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.bubbles.BubbleController; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; @@ -71,6 +68,7 @@ import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.shared.tracing.ProtoTraceable; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.EdgeBackGestureHandlerProto; @@ -207,6 +205,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa private boolean mUseMLModel; private float mMLModelThreshold; private String mPackageName; + private float mMLResults; private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver; @@ -386,7 +385,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa mGestureNavigationSettingsObserver.unregister(); mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); mPluginManager.removePluginListener(this); - ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); + TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); try { @@ -402,7 +401,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa updateDisplaySize(); mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, mContext.getMainThreadHandler()); - ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); + TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, runnable -> (mContext.getMainThreadHandler()).post(runnable), mOnPropertiesChangedListener); @@ -531,10 +530,10 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa new long[]{(long) y}, }; - final float results = mBackGestureTfClassifierProvider.predict(featuresVector); - if (results == -1) return -1; + mMLResults = mBackGestureTfClassifierProvider.predict(featuresVector); + if (mMLResults == -1) return -1; - return results >= mMLModelThreshold ? 1 : 0; + return mMLResults >= mMLModelThreshold ? 1 : 0; } private boolean isWithinTouchRegion(int x, int y) { @@ -604,6 +603,11 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa return; } mLogGesture = false; + String logPackageName = ""; + // Due to privacy, only top 100 most used apps by all users can be logged. + if (mUseMLModel && mVocab.containsKey(mPackageName) && mVocab.get(mPackageName) < 100) { + logPackageName = mPackageName; + } SysUiStatsLog.write(SysUiStatsLog.BACK_GESTURE_REPORTED_REPORTED, backType, (int) mDownPoint.y, mIsOnLeftEdge ? SysUiStatsLog.BACK_GESTURE__X_LOCATION__LEFT @@ -611,7 +615,8 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa (int) mDownPoint.x, (int) mDownPoint.y, (int) mEndPoint.x, (int) mEndPoint.y, mEdgeWidthLeft + mLeftInset, - mDisplaySize.x - (mEdgeWidthRight + mRightInset)); + mDisplaySize.x - (mEdgeWidthRight + mRightInset), + mUseMLModel ? mMLResults : -2, logPackageName); } private void onMotionEvent(MotionEvent ev) { @@ -621,6 +626,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa // either the bouncer is showing or the notification panel is hidden mInputEventReceiver.setBatchingEnabled(false); mIsOnLeftEdge = ev.getX() <= mEdgeWidthLeft + mLeftInset; + mMLResults = 0; mLogGesture = false; mInRejectedExclusion = false; mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed @@ -725,14 +731,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, InputDevice.SOURCE_KEYBOARD); - // Bubble controller will give us a valid display id if it should get the back event - BubbleController bubbleController = Dependency.get(BubbleController.class); - int bubbleDisplayId = bubbleController.getExpandedDisplayId(mContext); - if (bubbleDisplayId != INVALID_DISPLAY) { - ev.setDisplayId(bubbleDisplayId); - } else { - ev.setDisplayId(mContext.getDisplay().getDisplayId()); - } + ev.setDisplayId(mContext.getDisplay().getDisplayId()); InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java index 284f41a416d6..fbbda5f31093 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java @@ -16,6 +16,8 @@ package com.android.systemui.navigationbar.gestural; +import static android.view.Display.DEFAULT_DISPLAY; + import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Configuration; @@ -334,6 +336,7 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl .getDimension(R.dimen.navigation_edge_action_drag_threshold); setVisibility(GONE); + boolean isPrimaryDisplay = mContext.getDisplayId() == DEFAULT_DISPLAY; mRegionSamplingHelper = new RegionSamplingHelper(this, new RegionSamplingHelper.SamplingCallback() { @Override @@ -345,8 +348,14 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl public Rect getSampledRegion(View sampledView) { return mSamplingRect; } + + @Override + public boolean isSamplingEnabled() { + return isPrimaryDisplay; + } }); mRegionSamplingHelper.setWindowVisible(true); + mShowProtection = !isPrimaryDisplay; } @Override @@ -366,11 +375,6 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl updateIsDark(animate); } - private void setShowProtection(boolean showProtection) { - mShowProtection = showProtection; - invalidate(); - } - @Override public void setIsLeftPanel(boolean isLeftPanel) { mIsLeftPanel = isLeftPanel; diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java new file mode 100644 index 000000000000..eeb93bb7d766 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2020 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.people; + +import android.app.Activity; +import android.app.INotificationManager; +import android.app.people.IPeopleManager; +import android.content.Context; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.icu.text.MeasureFormat; +import android.icu.util.Measure; +import android.icu.util.MeasureUnit; +import android.os.Bundle; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.service.notification.ConversationChannelWrapper; +import android.util.Log; +import android.view.ViewGroup; + +import com.android.systemui.R; + +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +/** + * Shows the user their tiles for their priority People (go/live-status). + */ +public class PeopleSpaceActivity extends Activity { + + private static String sTAG = "PeopleSpaceActivity"; + + private ViewGroup mPeopleSpaceLayout; + private IPeopleManager mPeopleManager; + private INotificationManager mNotificationManager; + private PackageManager mPackageManager; + private LauncherApps mLauncherApps; + private List<ConversationChannelWrapper> mConversations; + private Context mContext; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.people_space_activity); + mPeopleSpaceLayout = findViewById(R.id.people_space_layout); + mContext = getApplicationContext(); + mNotificationManager = + INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); + mPackageManager = getPackageManager(); + mPeopleManager = IPeopleManager.Stub.asInterface( + ServiceManager.getService(Context.PEOPLE_SERVICE)); + mLauncherApps = mContext.getSystemService(LauncherApps.class); + setTileViewsWithPriorityConversations(); + } + + /** + * Retrieves all priority conversations and sets a {@link PeopleSpaceTileView}s for each + * priority conversation. + */ + private void setTileViewsWithPriorityConversations() { + try { + List<ConversationChannelWrapper> conversations = + mNotificationManager.getConversations( + true /* priority only */).getList(); + mConversations = conversations.stream().filter( + c -> shouldKeepConversation(c)).collect(Collectors.toList()); + for (ConversationChannelWrapper conversation : mConversations) { + PeopleSpaceTileView tileView = new PeopleSpaceTileView(mContext, + mPeopleSpaceLayout, + conversation.getShortcutInfo().getId()); + setTileView(tileView, conversation); + } + } catch (Exception e) { + Log.e(sTAG, "Couldn't retrieve conversations", e); + } + } + + /** Sets {@code tileView} with the data in {@code conversation}. */ + private void setTileView(PeopleSpaceTileView tileView, + ConversationChannelWrapper conversation) { + try { + ShortcutInfo shortcutInfo = conversation.getShortcutInfo(); + int userId = UserHandle.getUserHandleForUid( + conversation.getUid()).getIdentifier(); + + String pkg = shortcutInfo.getPackage(); + long lastInteraction = mPeopleManager.getLastInteraction( + pkg, userId, + shortcutInfo.getId()); + String status = lastInteraction != 0l ? mContext.getString( + R.string.last_interaction_status, + getLastInteractionString( + lastInteraction)) : mContext.getString(R.string.basic_status); + tileView.setStatus(status); + + tileView.setName(shortcutInfo.getLabel().toString()); + tileView.setPackageIcon(mPackageManager.getApplicationIcon(pkg)); + tileView.setPersonIcon(mLauncherApps.getShortcutIconDrawable(shortcutInfo, 0)); + tileView.setOnClickListener(mLauncherApps, shortcutInfo); + } catch (Exception e) { + Log.e(sTAG, "Couldn't retrieve shortcut information", e); + } + } + + /** Returns a readable representation of {@code lastInteraction}. */ + private String getLastInteractionString(long lastInteraction) { + long now = System.currentTimeMillis(); + Duration durationSinceLastInteraction = Duration.ofMillis( + now - lastInteraction); + MeasureFormat formatter = MeasureFormat.getInstance(Locale.getDefault(), + MeasureFormat.FormatWidth.WIDE); + if (durationSinceLastInteraction.toDays() >= 1) { + return + formatter + .formatMeasures(new Measure(durationSinceLastInteraction.toDays(), + MeasureUnit.DAY)); + } else if (durationSinceLastInteraction.toHours() >= 1) { + return formatter.formatMeasures(new Measure(durationSinceLastInteraction.toHours(), + MeasureUnit.HOUR)); + } else if (durationSinceLastInteraction.toMinutes() >= 1) { + return formatter.formatMeasures(new Measure(durationSinceLastInteraction.toMinutes(), + MeasureUnit.MINUTE)); + } else { + return formatter.formatMeasures( + new Measure(durationSinceLastInteraction.toMillis() / 1000, + MeasureUnit.SECOND)); + } + } + + /** + * Returns whether the {@code conversation} should be kept for display in the People Space. + * + * <p>A valid {@code conversation} must: + * <ul> + * <li>Have a non-null {@link ShortcutInfo} + * <li>Have an associated label in the {@link ShortcutInfo} + * </ul> + * </li> + */ + private boolean shouldKeepConversation(ConversationChannelWrapper conversation) { + ShortcutInfo shortcutInfo = conversation.getShortcutInfo(); + return shortcutInfo != null && shortcutInfo.getLabel().length() != 0; + } + + @Override + protected void onResume() { + super.onResume(); + // Refresh tile views to sync new conversations. + setTileViewsWithPriorityConversations(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java new file mode 100644 index 000000000000..d5ef190d5ff1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 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.people; + +import android.content.Context; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.systemui.R; + +/** + * PeopleSpaceTileView renders an individual person's tile with associated status. + */ +public class PeopleSpaceTileView extends LinearLayout { + + private View mTileView; + private TextView mNameView; + private TextView mStatusView; + private ImageView mPackageIconView; + private ImageView mPersonIconView; + + public PeopleSpaceTileView(Context context, ViewGroup view, String shortcutId) { + super(context); + mTileView = view.findViewWithTag(shortcutId); + if (mTileView == null) { + LayoutInflater inflater = LayoutInflater.from(context); + mTileView = inflater.inflate(R.layout.people_space_tile_view, view, false); + view.addView(mTileView, LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + mTileView.setTag(shortcutId); + } + mNameView = mTileView.findViewById(R.id.tile_view_name); + mStatusView = mTileView.findViewById(R.id.tile_view_status); + mPackageIconView = mTileView.findViewById(R.id.tile_view_package_icon); + mPersonIconView = mTileView.findViewById(R.id.tile_view_person_icon); + } + + /** Sets the name text on the tile. */ + public void setName(String name) { + mNameView.setText(name); + } + + /** Sets the status text on the tile. */ + public void setStatus(String status) { + mStatusView.setText(status); + } + + /** Sets the package drawable on the tile. */ + public void setPackageIcon(Drawable drawable) { + mPackageIconView.setImageDrawable(drawable); + } + + /** Sets the person drawable on the tile. */ + public void setPersonIcon(Drawable drawable) { + mPersonIconView.setImageDrawable(drawable); + } + + /** Sets the click listener of the tile. */ + public void setOnClickListener(LauncherApps launcherApps, ShortcutInfo shortcutInfo) { + mTileView.setOnClickListener(v -> launcherApps.startShortcut(shortcutInfo, null, null)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/pip/Pip.java b/packages/SystemUI/src/com/android/systemui/pip/Pip.java deleted file mode 100644 index b068370da9e3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/Pip.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2017 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.pip; - -import android.content.res.Configuration; -import android.media.session.MediaController; - -import com.android.systemui.pip.tv.PipController; -import com.android.systemui.shared.recents.IPinnedStackAnimationListener; - -import java.io.PrintWriter; - -/** - * Interface to engage picture in picture feature. - */ -public interface Pip { - /** - * Called when showing Pip menu. - */ - void showPictureInPictureMenu(); - - /** - * Registers {@link com.android.systemui.pip.tv.PipController.Listener} that gets called. - * whenever receiving notification on changes in PIP. - */ - default void addListener(PipController.Listener listener) { - } - - /** - * Registers a {@link com.android.systemui.pip.tv.PipController.MediaListener} to PipController. - */ - default void addMediaListener(PipController.MediaListener listener) { - } - - /** - * Closes PIP (PIPed activity and PIP system UI). - */ - default void closePip() { - } - - /** - * Expand PIP, it's possible that specific request to activate the window via Alt-tab. - */ - default void expandPip() { - } - - /** - * Get current play back state. (e.g: Used in TV) - * - * @return The state of defined in PipController. - */ - default int getPlaybackState() { - return 0; - } - - /** - * Get MediaController. - * - * @return The MediaController instance. - */ - default MediaController getMediaController() { - return null; - } - - /** - * Hides the PIP menu. - */ - void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback); - - /** - * Returns {@code true} if PIP is shown. - */ - default boolean isPipShown() { - return false; - } - - /** - * Moves the PIPed activity to the fullscreen and closes PIP system UI. - */ - default void movePipToFullscreen() { - } - - /** - * Called when configuration change invoked. - */ - void onConfigurationChanged(Configuration newConfig); - - /** - * Removes a {@link PipController.Listener} from PipController. - */ - default void removeListener(PipController.Listener listener) { - } - - /** - * Removes a {@link com.android.systemui.pip.tv.PipController.MediaListener} from PipController. - */ - default void removeMediaListener(PipController.MediaListener listener) { - } - - /** - * Resize the Pip to the appropriate size for the input state. - * - * @param state In Pip state also used to determine the new size for the Pip. - */ - default void resizePinnedStack(int state) { - } - - /** - * Resumes resizing operation on the Pip that was previously suspended. - * - * @param reason The reason resizing operations on the Pip was suspended. - */ - default void resumePipResizing(int reason) { - } - - /** - * Sets both shelf visibility and its height. - * - * @param visible visibility of shelf. - * @param height to specify the height for shelf. - */ - default void setShelfHeight(boolean visible, int height) { - } - - /** - * Set the pinned stack with {@link PipAnimationController.AnimationType} - * - * @param animationType The pre-defined {@link PipAnimationController.AnimationType} - */ - default void setPinnedStackAnimationType(int animationType) { - } - - /** - * Registers the pinned stack animation listener. - * - * @param listener The listener of pinned stack animation. - */ - default void setPinnedStackAnimationListener(IPinnedStackAnimationListener listener) { - } - - /** - * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called. - * - * @param reason The reason for suspending resizing operations on the Pip. - */ - default void suspendPipResizing(int reason) { - } - - /** - * Dump the current state and information if need. - * - * @param pw The stream to dump information to. - */ - default void dump(PrintWriter pw) { - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java deleted file mode 100644 index 419d8d590124..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java +++ /dev/null @@ -1,448 +0,0 @@ -/* - * Copyright (C) 2020 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.pip; - -import android.animation.AnimationHandler; -import android.animation.Animator; -import android.animation.RectEvaluator; -import android.animation.ValueAnimator; -import android.annotation.IntDef; -import android.graphics.Rect; -import android.view.SurfaceControl; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.graphics.SfVsyncFrameCallbackProvider; -import com.android.systemui.Interpolators; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Controller class of PiP animations (both from and to PiP mode). - */ -public class PipAnimationController { - private static final float FRACTION_START = 0f; - private static final float FRACTION_END = 1f; - - public static final int ANIM_TYPE_BOUNDS = 0; - public static final int ANIM_TYPE_ALPHA = 1; - - @IntDef(prefix = { "ANIM_TYPE_" }, value = { - ANIM_TYPE_BOUNDS, - ANIM_TYPE_ALPHA - }) - @Retention(RetentionPolicy.SOURCE) - public @interface AnimationType {} - - public static final int TRANSITION_DIRECTION_NONE = 0; - public static final int TRANSITION_DIRECTION_SAME = 1; - public static final int TRANSITION_DIRECTION_TO_PIP = 2; - public static final int TRANSITION_DIRECTION_LEAVE_PIP = 3; - public static final int TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN = 4; - public static final int TRANSITION_DIRECTION_REMOVE_STACK = 5; - - @IntDef(prefix = { "TRANSITION_DIRECTION_" }, value = { - TRANSITION_DIRECTION_NONE, - TRANSITION_DIRECTION_SAME, - TRANSITION_DIRECTION_TO_PIP, - TRANSITION_DIRECTION_LEAVE_PIP, - TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN, - TRANSITION_DIRECTION_REMOVE_STACK - }) - @Retention(RetentionPolicy.SOURCE) - public @interface TransitionDirection {} - - public static boolean isInPipDirection(@TransitionDirection int direction) { - return direction == TRANSITION_DIRECTION_TO_PIP; - } - - public static boolean isOutPipDirection(@TransitionDirection int direction) { - return direction == TRANSITION_DIRECTION_LEAVE_PIP - || direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; - } - - private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; - - private PipTransitionAnimator mCurrentAnimator; - - private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = - ThreadLocal.withInitial(() -> { - AnimationHandler handler = new AnimationHandler(); - handler.setProvider(new SfVsyncFrameCallbackProvider()); - return handler; - }); - - PipAnimationController(PipSurfaceTransactionHelper helper) { - mSurfaceTransactionHelper = helper; - } - - @SuppressWarnings("unchecked") - PipTransitionAnimator getAnimator(SurfaceControl leash, - Rect destinationBounds, float alphaStart, float alphaEnd) { - if (mCurrentAnimator == null) { - mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd)); - } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA - && mCurrentAnimator.isRunning()) { - mCurrentAnimator.updateEndValue(alphaEnd); - } else { - mCurrentAnimator.cancel(); - mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd)); - } - return mCurrentAnimator; - } - - @SuppressWarnings("unchecked") - PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds, - Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction) { - if (mCurrentAnimator == null) { - mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect, - direction)); - } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA - && mCurrentAnimator.isRunning()) { - // If we are still animating the fade into pip, then just move the surface and ensure - // we update with the new destination bounds, but don't interrupt the existing animation - // with a new bounds - mCurrentAnimator.setDestinationBounds(endBounds); - } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_BOUNDS - && mCurrentAnimator.isRunning()) { - mCurrentAnimator.setDestinationBounds(endBounds); - // construct new Rect instances in case they are recycled - mCurrentAnimator.updateEndValue(new Rect(endBounds)); - } else { - mCurrentAnimator.cancel(); - mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect, - direction)); - } - return mCurrentAnimator; - } - - PipTransitionAnimator getCurrentAnimator() { - return mCurrentAnimator; - } - - private PipTransitionAnimator setupPipTransitionAnimator(PipTransitionAnimator animator) { - animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper); - animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - animator.setFloatValues(FRACTION_START, FRACTION_END); - animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get()); - return animator; - } - - /** - * Additional callback interface for PiP animation - */ - public static class PipAnimationCallback { - /** - * Called when PiP animation is started. - */ - public void onPipAnimationStart(PipTransitionAnimator animator) {} - - /** - * Called when PiP animation is ended. - */ - public void onPipAnimationEnd(SurfaceControl.Transaction tx, - PipTransitionAnimator animator) {} - - /** - * Called when PiP animation is cancelled. - */ - public void onPipAnimationCancel(PipTransitionAnimator animator) {} - } - - /** - * Animator for PiP transition animation which supports both alpha and bounds animation. - * @param <T> Type of property to animate, either alpha (float) or bounds (Rect) - */ - public abstract static class PipTransitionAnimator<T> extends ValueAnimator implements - ValueAnimator.AnimatorUpdateListener, - ValueAnimator.AnimatorListener { - private final SurfaceControl mLeash; - private final @AnimationType int mAnimationType; - private final Rect mDestinationBounds = new Rect(); - - protected T mCurrentValue; - protected T mStartValue; - private T mEndValue; - private PipAnimationCallback mPipAnimationCallback; - private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory - mSurfaceControlTransactionFactory; - private PipSurfaceTransactionHelper mSurfaceTransactionHelper; - private @TransitionDirection int mTransitionDirection; - - private PipTransitionAnimator(SurfaceControl leash, @AnimationType int animationType, - Rect destinationBounds, T startValue, T endValue) { - mLeash = leash; - mAnimationType = animationType; - mDestinationBounds.set(destinationBounds); - mStartValue = startValue; - mEndValue = endValue; - addListener(this); - addUpdateListener(this); - mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; - mTransitionDirection = TRANSITION_DIRECTION_NONE; - } - - @Override - public void onAnimationStart(Animator animation) { - mCurrentValue = mStartValue; - onStartTransaction(mLeash, newSurfaceControlTransaction()); - if (mPipAnimationCallback != null) { - mPipAnimationCallback.onPipAnimationStart(this); - } - } - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(), - animation.getAnimatedFraction()); - } - - @Override - public void onAnimationEnd(Animator animation) { - mCurrentValue = mEndValue; - final SurfaceControl.Transaction tx = newSurfaceControlTransaction(); - onEndTransaction(mLeash, tx); - if (mPipAnimationCallback != null) { - mPipAnimationCallback.onPipAnimationEnd(tx, this); - } - } - - @Override - public void onAnimationCancel(Animator animation) { - if (mPipAnimationCallback != null) { - mPipAnimationCallback.onPipAnimationCancel(this); - } - } - - @Override public void onAnimationRepeat(Animator animation) {} - - @AnimationType int getAnimationType() { - return mAnimationType; - } - - PipTransitionAnimator<T> setPipAnimationCallback(PipAnimationCallback callback) { - mPipAnimationCallback = callback; - return this; - } - - @TransitionDirection int getTransitionDirection() { - return mTransitionDirection; - } - - PipTransitionAnimator<T> setTransitionDirection(@TransitionDirection int direction) { - if (direction != TRANSITION_DIRECTION_SAME) { - mTransitionDirection = direction; - } - return this; - } - - T getStartValue() { - return mStartValue; - } - - T getEndValue() { - return mEndValue; - } - - Rect getDestinationBounds() { - return mDestinationBounds; - } - - void setDestinationBounds(Rect destinationBounds) { - mDestinationBounds.set(destinationBounds); - if (mAnimationType == ANIM_TYPE_ALPHA) { - onStartTransaction(mLeash, newSurfaceControlTransaction()); - } - } - - void setCurrentValue(T value) { - mCurrentValue = value; - } - - boolean shouldApplyCornerRadius() { - return !isOutPipDirection(mTransitionDirection); - } - - boolean inScaleTransition() { - if (mAnimationType != ANIM_TYPE_BOUNDS) return false; - final int direction = getTransitionDirection(); - return !isInPipDirection(direction) && !isOutPipDirection(direction); - } - - /** - * Updates the {@link #mEndValue}. - * - * NOTE: Do not forget to call {@link #setDestinationBounds(Rect)} for bounds animation. - * This is typically used when we receive a shelf height adjustment during the bounds - * animation. In which case we can update the end bounds and keep the existing animation - * running instead of cancelling it. - */ - void updateEndValue(T endValue) { - mEndValue = endValue; - } - - SurfaceControl.Transaction newSurfaceControlTransaction() { - return mSurfaceControlTransactionFactory.getTransaction(); - } - - @VisibleForTesting - void setSurfaceControlTransactionFactory( - PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { - mSurfaceControlTransactionFactory = factory; - } - - PipSurfaceTransactionHelper getSurfaceTransactionHelper() { - return mSurfaceTransactionHelper; - } - - void setSurfaceTransactionHelper(PipSurfaceTransactionHelper helper) { - mSurfaceTransactionHelper = helper; - } - - void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {} - - void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {} - - abstract void applySurfaceControlTransaction(SurfaceControl leash, - SurfaceControl.Transaction tx, float fraction); - - static PipTransitionAnimator<Float> ofAlpha(SurfaceControl leash, - Rect destinationBounds, float startValue, float endValue) { - return new PipTransitionAnimator<Float>(leash, ANIM_TYPE_ALPHA, - destinationBounds, startValue, endValue) { - @Override - void applySurfaceControlTransaction(SurfaceControl leash, - SurfaceControl.Transaction tx, float fraction) { - final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction; - setCurrentValue(alpha); - getSurfaceTransactionHelper().alpha(tx, leash, alpha); - tx.apply(); - } - - @Override - void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { - if (getTransitionDirection() == TRANSITION_DIRECTION_REMOVE_STACK) { - // while removing the pip stack, no extra work needs to be done here. - return; - } - getSurfaceTransactionHelper() - .resetScale(tx, leash, getDestinationBounds()) - .crop(tx, leash, getDestinationBounds()) - .round(tx, leash, shouldApplyCornerRadius()); - tx.show(leash); - tx.apply(); - } - - @Override - void updateEndValue(Float endValue) { - super.updateEndValue(endValue); - mStartValue = mCurrentValue; - } - }; - } - - static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash, - Rect startValue, Rect endValue, Rect sourceHintRect, - @PipAnimationController.TransitionDirection int direction) { - // Just for simplicity we'll interpolate between the source rect hint insets and empty - // insets to calculate the window crop - final Rect initialSourceValue; - if (isOutPipDirection(direction)) { - initialSourceValue = new Rect(endValue); - } else { - initialSourceValue = new Rect(startValue); - } - - final Rect sourceHintRectInsets; - if (sourceHintRect == null) { - sourceHintRectInsets = null; - } else { - sourceHintRectInsets = new Rect(sourceHintRect.left - initialSourceValue.left, - sourceHintRect.top - initialSourceValue.top, - initialSourceValue.right - sourceHintRect.right, - initialSourceValue.bottom - sourceHintRect.bottom); - } - final Rect sourceInsets = new Rect(0, 0, 0, 0); - - // construct new Rect instances in case they are recycled - return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS, - endValue, new Rect(startValue), new Rect(endValue)) { - private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); - private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); - - @Override - void applySurfaceControlTransaction(SurfaceControl leash, - SurfaceControl.Transaction tx, float fraction) { - final Rect start = getStartValue(); - final Rect end = getEndValue(); - Rect bounds = mRectEvaluator.evaluate(fraction, start, end); - setCurrentValue(bounds); - if (inScaleTransition() || sourceHintRect == null) { - if (isOutPipDirection(direction)) { - getSurfaceTransactionHelper().scale(tx, leash, end, bounds); - } else { - getSurfaceTransactionHelper().scale(tx, leash, start, bounds); - } - } else { - final Rect insets; - if (isOutPipDirection(direction)) { - insets = mInsetsEvaluator.evaluate(fraction, sourceHintRectInsets, - sourceInsets); - } else { - insets = mInsetsEvaluator.evaluate(fraction, sourceInsets, - sourceHintRectInsets); - } - getSurfaceTransactionHelper().scaleAndCrop(tx, leash, - initialSourceValue, bounds, insets); - } - tx.apply(); - } - - @Override - void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { - getSurfaceTransactionHelper() - .alpha(tx, leash, 1f) - .round(tx, leash, shouldApplyCornerRadius()); - tx.show(leash); - tx.apply(); - } - - @Override - void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { - // NOTE: intentionally does not apply the transaction here. - // this end transaction should get executed synchronously with the final - // WindowContainerTransaction in task organizer - getSurfaceTransactionHelper() - .resetScale(tx, leash, getDestinationBounds()) - .crop(tx, leash, getDestinationBounds()); - } - - @Override - void updateEndValue(Rect endValue) { - super.updateEndValue(endValue); - if (mStartValue != null && mCurrentValue != null) { - mStartValue.set(mCurrentValue); - } - } - }; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java deleted file mode 100644 index 89b5c38d94a7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java +++ /dev/null @@ -1,553 +0,0 @@ -/* - * Copyright (C) 2019 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.pip; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.util.TypedValue.COMPLEX_UNIT_DIP; -import static android.view.Surface.ROTATION_0; -import static android.view.Surface.ROTATION_180; - -import android.app.ActivityTaskManager; -import android.app.ActivityTaskManager.RootTaskInfo; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.RemoteException; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.Size; -import android.util.TypedValue; -import android.view.Display; -import android.view.DisplayInfo; -import android.view.Gravity; -import android.window.WindowContainerTransaction; - -import com.android.systemui.dagger.SysUISingleton; -import com.android.wm.shell.common.DisplayLayout; - -import java.io.PrintWriter; - -/** - * Handles bounds calculation for PIP on Phone and other form factors, it keeps tracking variant - * state changes originated from Window Manager and is the source of truth for PiP window bounds. - */ -@SysUISingleton -public class PipBoundsHandler { - - private static final String TAG = PipBoundsHandler.class.getSimpleName(); - private static final float INVALID_SNAP_FRACTION = -1f; - - private final PipSnapAlgorithm mSnapAlgorithm; - private final DisplayInfo mDisplayInfo = new DisplayInfo(); - private DisplayLayout mDisplayLayout; - - private ComponentName mLastPipComponentName; - private float mReentrySnapFraction = INVALID_SNAP_FRACTION; - private Size mReentrySize; - - private float mDefaultAspectRatio; - private float mMinAspectRatio; - private float mMaxAspectRatio; - private float mAspectRatio; - private int mDefaultStackGravity; - private int mDefaultMinSize; - private Point mScreenEdgeInsets; - private int mCurrentMinSize; - private Size mOverrideMinimalSize; - - private boolean mIsImeShowing; - private int mImeHeight; - private boolean mIsShelfShowing; - private int mShelfHeight; - - public PipBoundsHandler(Context context) { - mSnapAlgorithm = new PipSnapAlgorithm(context); - mDisplayLayout = new DisplayLayout(); - reloadResources(context); - // Initialize the aspect ratio to the default aspect ratio. Don't do this in reload - // resources as it would clobber mAspectRatio when entering PiP from fullscreen which - // triggers a configuration change and the resources to be reloaded. - mAspectRatio = mDefaultAspectRatio; - } - - /** - * TODO: move the resources to SysUI package. - */ - private void reloadResources(Context context) { - final Resources res = context.getResources(); - mDefaultAspectRatio = res.getFloat( - com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio); - mDefaultStackGravity = res.getInteger( - com.android.internal.R.integer.config_defaultPictureInPictureGravity); - mDefaultMinSize = res.getDimensionPixelSize( - com.android.internal.R.dimen.default_minimal_size_pip_resizable_task); - mCurrentMinSize = mDefaultMinSize; - final String screenEdgeInsetsDpString = res.getString( - com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets); - final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty() - ? Size.parseSize(screenEdgeInsetsDpString) - : null; - mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point() - : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), res.getDisplayMetrics()), - dpToPx(screenEdgeInsetsDp.getHeight(), res.getDisplayMetrics())); - mMinAspectRatio = res.getFloat( - com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio); - mMaxAspectRatio = res.getFloat( - com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio); - } - - /** - * Sets or update latest {@link DisplayLayout} when new display added or rotation callbacks - * from {@link DisplayController.OnDisplaysChangedListener} - * @param newDisplayLayout latest {@link DisplayLayout} - */ - public void setDisplayLayout(DisplayLayout newDisplayLayout) { - mDisplayLayout.set(newDisplayLayout); - } - - /** - * Update the Min edge size for {@link PipSnapAlgorithm} to calculate corresponding bounds - * @param minEdgeSize - */ - public void setMinEdgeSize(int minEdgeSize) { - mCurrentMinSize = minEdgeSize; - } - - protected float getAspectRatio() { - return mAspectRatio; - } - - /** - * Sets both shelf visibility and its height if applicable. - * @return {@code true} if the internal shelf state is changed, {@code false} otherwise. - */ - public boolean setShelfHeight(boolean shelfVisible, int shelfHeight) { - final boolean shelfShowing = shelfVisible && shelfHeight > 0; - if (shelfShowing == mIsShelfShowing && shelfHeight == mShelfHeight) { - return false; - } - - mIsShelfShowing = shelfVisible; - mShelfHeight = shelfHeight; - return true; - } - - /** - * Responds to IPinnedStackListener on IME visibility change. - */ - public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { - mIsImeShowing = imeVisible; - mImeHeight = imeHeight; - } - - /** - * Responds to IPinnedStackListener on movement bounds change. - * Note that both inset and normal bounds will be calculated here rather than in the caller. - */ - public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, - Rect animatingBounds, DisplayInfo displayInfo) { - getInsetBounds(insetBounds); - final Rect defaultBounds = getDefaultBounds(INVALID_SNAP_FRACTION, null); - normalBounds.set(defaultBounds); - if (animatingBounds.isEmpty()) { - animatingBounds.set(defaultBounds); - } - if (isValidPictureInPictureAspectRatio(mAspectRatio)) { - transformBoundsToAspectRatio(normalBounds, mAspectRatio, - false /* useCurrentMinEdgeSize */, false /* useCurrentSize */); - } - displayInfo.copyFrom(mDisplayInfo); - } - - /** - * Responds to IPinnedStackListener on saving reentry snap fraction and size - * for a given {@link ComponentName}. - */ - public void onSaveReentryBounds(ComponentName componentName, Rect bounds) { - mReentrySnapFraction = getSnapFraction(bounds); - mReentrySize = new Size(bounds.width(), bounds.height()); - mLastPipComponentName = componentName; - } - - /** - * Responds to IPinnedStackListener on resetting reentry snap fraction and size - * for a given {@link ComponentName}. - */ - public void onResetReentryBounds(ComponentName componentName) { - if (componentName.equals(mLastPipComponentName)) { - onResetReentryBoundsUnchecked(); - } - } - - private void onResetReentryBoundsUnchecked() { - mReentrySnapFraction = INVALID_SNAP_FRACTION; - mReentrySize = null; - mLastPipComponentName = null; - } - - /** - * Returns ture if there's a valid snap fraction. This is used with {@link EXTRA_IS_FIRST_ENTRY} - * to see if this is the first time user has entered PIP for the component. - */ - public boolean hasSaveReentryBounds() { - return mReentrySnapFraction != INVALID_SNAP_FRACTION; - } - - /** - * The {@link PipSnapAlgorithm} is couple on display bounds - * @return {@link PipSnapAlgorithm}. - */ - public PipSnapAlgorithm getSnapAlgorithm() { - return mSnapAlgorithm; - } - - public Rect getDisplayBounds() { - return new Rect(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); - } - - public int getDisplayRotation() { - return mDisplayInfo.rotation; - } - - /** - * Responds to IPinnedStackListener on {@link DisplayInfo} change. - * It will normally follow up with a - * {@link #onMovementBoundsChanged(Rect, Rect, Rect, DisplayInfo)} callback. - */ - public void onDisplayInfoChanged(DisplayInfo displayInfo) { - mDisplayInfo.copyFrom(displayInfo); - } - - /** - * Responds to IPinnedStackListener on configuration change. - */ - public void onConfigurationChanged(Context context) { - reloadResources(context); - } - - /** - * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window. - * It will normally follow up with a - * {@link #onMovementBoundsChanged(Rect, Rect, Rect, DisplayInfo)} callback. - */ - public void onAspectRatioChanged(float aspectRatio) { - mAspectRatio = aspectRatio; - } - - /** - * See {@link #getDestinationBounds(ComponentName, float, Rect, Size, boolean)} - */ - Rect getDestinationBounds(ComponentName componentName, float aspectRatio, Rect bounds, - Size minimalSize) { - return getDestinationBounds(componentName, aspectRatio, bounds, minimalSize, - false /* useCurrentMinEdgeSize */); - } - - /** - * @return {@link Rect} of the destination PiP window bounds. - */ - Rect getDestinationBounds(ComponentName componentName, float aspectRatio, Rect bounds, - Size minimalSize, boolean useCurrentMinEdgeSize) { - if (!componentName.equals(mLastPipComponentName)) { - onResetReentryBoundsUnchecked(); - mLastPipComponentName = componentName; - } - final Rect destinationBounds; - if (bounds == null) { - final Rect defaultBounds = getDefaultBounds(mReentrySnapFraction, mReentrySize); - destinationBounds = new Rect(defaultBounds); - if (mReentrySnapFraction == INVALID_SNAP_FRACTION && mReentrySize == null) { - mOverrideMinimalSize = minimalSize; - } - } else { - destinationBounds = new Rect(bounds); - } - if (isValidPictureInPictureAspectRatio(aspectRatio)) { - boolean useCurrentSize = bounds == null && mReentrySize != null; - transformBoundsToAspectRatio(destinationBounds, aspectRatio, useCurrentMinEdgeSize, - useCurrentSize); - } - mAspectRatio = aspectRatio; - return destinationBounds; - } - - float getDefaultAspectRatio() { - return mDefaultAspectRatio; - } - - public void onOverlayChanged(Context context, Display display) { - mDisplayLayout = new DisplayLayout(context, display); - } - - /** - * Updatest the display info and display layout on rotation change. This is needed even when we - * aren't in PIP because the rotation layout is used to calculate the proper insets for the - * next enter animation into PIP. - */ - public void onDisplayRotationChangedNotInPip(Context context, int toRotation) { - // Update the display layout, note that we have to do this on every rotation even if we - // aren't in PIP since we need to update the display layout to get the right resources - mDisplayLayout.rotateTo(context.getResources(), toRotation); - - // Populate the new {@link #mDisplayInfo}. - // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation, - // therefore, the width/height may require a swap first. - // Moving forward, we should get the new dimensions after rotation from DisplayLayout. - mDisplayInfo.rotation = toRotation; - updateDisplayInfoIfNeeded(); - } - - /** - * Updates the display info, calculating and returning the new stack and movement bounds in the - * new orientation of the device if necessary. - * - * @return {@code true} if internal {@link DisplayInfo} is rotated, {@code false} otherwise. - */ - public boolean onDisplayRotationChanged(Context context, Rect outBounds, Rect oldBounds, - Rect outInsetBounds, - int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) { - // Bail early if the event is not sent to current {@link #mDisplayInfo} - if ((displayId != mDisplayInfo.displayId) || (fromRotation == toRotation)) { - return false; - } - - // Bail early if the pinned task is staled. - final RootTaskInfo pinnedTaskInfo; - try { - pinnedTaskInfo = ActivityTaskManager.getService() - .getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); - if (pinnedTaskInfo == null) return false; - } catch (RemoteException e) { - Log.e(TAG, "Failed to get RootTaskInfo for pinned task", e); - return false; - } - - // Calculate the snap fraction of the current stack along the old movement bounds - final Rect postChangeStackBounds = new Rect(oldBounds); - final float snapFraction = getSnapFraction(postChangeStackBounds); - - // Update the display layout - mDisplayLayout.rotateTo(context.getResources(), toRotation); - - // Populate the new {@link #mDisplayInfo}. - // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation, - // therefore, the width/height may require a swap first. - // Moving forward, we should get the new dimensions after rotation from DisplayLayout. - mDisplayInfo.rotation = toRotation; - updateDisplayInfoIfNeeded(); - - // Calculate the stack bounds in the new orientation based on same fraction along the - // rotated movement bounds. - final Rect postChangeMovementBounds = getMovementBounds(postChangeStackBounds, - false /* adjustForIme */); - mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, - snapFraction); - - getInsetBounds(outInsetBounds); - outBounds.set(postChangeStackBounds); - t.setBounds(pinnedTaskInfo.token, outBounds); - return true; - } - - private void updateDisplayInfoIfNeeded() { - final boolean updateNeeded; - if ((mDisplayInfo.rotation == ROTATION_0) || (mDisplayInfo.rotation == ROTATION_180)) { - updateNeeded = (mDisplayInfo.logicalWidth > mDisplayInfo.logicalHeight); - } else { - updateNeeded = (mDisplayInfo.logicalWidth < mDisplayInfo.logicalHeight); - } - if (updateNeeded) { - final int newLogicalHeight = mDisplayInfo.logicalWidth; - mDisplayInfo.logicalWidth = mDisplayInfo.logicalHeight; - mDisplayInfo.logicalHeight = newLogicalHeight; - } - } - - /** - * @return whether the given {@param aspectRatio} is valid. - */ - private boolean isValidPictureInPictureAspectRatio(float aspectRatio) { - return Float.compare(mMinAspectRatio, aspectRatio) <= 0 - && Float.compare(aspectRatio, mMaxAspectRatio) <= 0; - } - - /** - * Sets the current bound with the currently store aspect ratio. - * @param stackBounds - */ - public void transformBoundsToAspectRatio(Rect stackBounds) { - transformBoundsToAspectRatio(stackBounds, mAspectRatio, true /* useCurrentMinEdgeSize */, - true /* useCurrentSize */); - } - - /** - * Set the current bounds (or the default bounds if there are no current bounds) with the - * specified aspect ratio. - */ - private void transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio, - boolean useCurrentMinEdgeSize, boolean useCurrentSize) { - // Save the snap fraction and adjust the size based on the new aspect ratio. - final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds, - getMovementBounds(stackBounds)); - final int minEdgeSize = useCurrentMinEdgeSize ? mCurrentMinSize : mDefaultMinSize; - final Size size; - if (useCurrentMinEdgeSize || useCurrentSize) { - size = mSnapAlgorithm.getSizeForAspectRatio( - new Size(stackBounds.width(), stackBounds.height()), aspectRatio, minEdgeSize); - } else { - size = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, minEdgeSize, - mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); - } - - final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f); - final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f); - stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight()); - // apply the override minimal size if applicable, this minimal size is specified by app - if (mOverrideMinimalSize != null) { - transformBoundsToMinimalSize(stackBounds, aspectRatio, mOverrideMinimalSize); - } - mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction); - } - - /** - * Transforms a given bounds to meet the minimal size constraints. - * This function assumes the given {@param stackBounds} qualifies {@param aspectRatio}. - */ - private void transformBoundsToMinimalSize(Rect stackBounds, float aspectRatio, - Size minimalSize) { - if (minimalSize == null) return; - final Size adjustedMinimalSize; - final float minimalSizeAspectRatio = - minimalSize.getWidth() / (float) minimalSize.getHeight(); - if (minimalSizeAspectRatio > aspectRatio) { - // minimal size is wider, fixed the width and increase the height - adjustedMinimalSize = new Size( - minimalSize.getWidth(), (int) (minimalSize.getWidth() / aspectRatio)); - } else { - adjustedMinimalSize = new Size( - (int) (minimalSize.getHeight() * aspectRatio), minimalSize.getHeight()); - } - final Rect containerBounds = new Rect(stackBounds); - Gravity.apply(mDefaultStackGravity, - adjustedMinimalSize.getWidth(), adjustedMinimalSize.getHeight(), - containerBounds, stackBounds); - } - - /** - * @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are - * provided, then it will apply the default bounds to the provided snap fraction and size. - */ - private Rect getDefaultBounds(float snapFraction, Size size) { - final Rect defaultBounds = new Rect(); - if (snapFraction != INVALID_SNAP_FRACTION && size != null) { - defaultBounds.set(0, 0, size.getWidth(), size.getHeight()); - final Rect movementBounds = getMovementBounds(defaultBounds); - mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction); - } else { - final Rect insetBounds = new Rect(); - getInsetBounds(insetBounds); - size = mSnapAlgorithm.getSizeForAspectRatio(mDefaultAspectRatio, - mDefaultMinSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); - Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds, - 0, Math.max(mIsImeShowing ? mImeHeight : 0, - mIsShelfShowing ? mShelfHeight : 0), - defaultBounds); - } - return defaultBounds; - } - - /** - * Populates the bounds on the screen that the PIP can be visible in. - */ - protected void getInsetBounds(Rect outRect) { - Rect insets = mDisplayLayout.stableInsets(); - outRect.set(insets.left + mScreenEdgeInsets.x, - insets.top + mScreenEdgeInsets.y, - mDisplayInfo.logicalWidth - insets.right - mScreenEdgeInsets.x, - mDisplayInfo.logicalHeight - insets.bottom - mScreenEdgeInsets.y); - } - - /** - * @return the movement bounds for the given {@param stackBounds} and the current state of the - * controller. - */ - private Rect getMovementBounds(Rect stackBounds) { - return getMovementBounds(stackBounds, true /* adjustForIme */); - } - - /** - * @return the movement bounds for the given {@param stackBounds} and the current state of the - * controller. - */ - private Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) { - final Rect movementBounds = new Rect(); - getInsetBounds(movementBounds); - - // Apply the movement bounds adjustments based on the current state. - mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds, - (adjustForIme && mIsImeShowing) ? mImeHeight : 0); - return movementBounds; - } - - /** - * @return the default snap fraction to apply instead of the default gravity when calculating - * the default stack bounds when first entering PiP. - */ - public float getSnapFraction(Rect stackBounds) { - return mSnapAlgorithm.getSnapFraction(stackBounds, getMovementBounds(stackBounds)); - } - - /** - * Applies the given snap fraction to the given stack bounds. - */ - public void applySnapFraction(Rect stackBounds, float snapFraction) { - final Rect movementBounds = getMovementBounds(stackBounds); - mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction); - } - - /** - * @return the pixels for a given dp value. - */ - private int dpToPx(float dpValue, DisplayMetrics dm) { - return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); - } - - /** - * Dumps internal states. - */ - public void dump(PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.println(prefix + TAG); - pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName); - pw.println(innerPrefix + "mReentrySnapFraction=" + mReentrySnapFraction); - pw.println(innerPrefix + "mReentrySize=" + mReentrySize); - pw.println(innerPrefix + "mDisplayInfo=" + mDisplayInfo); - pw.println(innerPrefix + "mDefaultAspectRatio=" + mDefaultAspectRatio); - pw.println(innerPrefix + "mMinAspectRatio=" + mMinAspectRatio); - pw.println(innerPrefix + "mMaxAspectRatio=" + mMaxAspectRatio); - pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio); - pw.println(innerPrefix + "mDefaultStackGravity=" + mDefaultStackGravity); - pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); - pw.println(innerPrefix + "mImeHeight=" + mImeHeight); - pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); - pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); - pw.println(innerPrefix + "mSnapAlgorithm" + mSnapAlgorithm); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipSnapAlgorithm.java b/packages/SystemUI/src/com/android/systemui/pip/PipSnapAlgorithm.java deleted file mode 100644 index 5d23e4207c33..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/PipSnapAlgorithm.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (C) 2016 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.pip; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.PointF; -import android.graphics.Rect; -import android.util.Size; - -/** - * Calculates the snap targets and the snap position for the PIP given a position and a velocity. - * All bounds are relative to the display top/left. - */ -public class PipSnapAlgorithm { - - private final float mDefaultSizePercent; - private final float mMinAspectRatioForMinSize; - private final float mMaxAspectRatioForMinSize; - - public PipSnapAlgorithm(Context context) { - Resources res = context.getResources(); - mDefaultSizePercent = res.getFloat( - com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent); - mMaxAspectRatioForMinSize = res.getFloat( - com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize); - mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize; - } - - /** - * @return returns a fraction that describes where along the {@param movementBounds} the - * {@param stackBounds} are. If the {@param stackBounds} are not currently on the - * {@param movementBounds} exactly, then they will be snapped to the movement bounds. - * - * The fraction is defined in a clockwise fashion against the {@param movementBounds}: - * - * 0 1 - * 4 +---+ 1 - * | | - * 3 +---+ 2 - * 3 2 - */ - public float getSnapFraction(Rect stackBounds, Rect movementBounds) { - final Rect tmpBounds = new Rect(); - snapRectToClosestEdge(stackBounds, movementBounds, tmpBounds); - final float widthFraction = (float) (tmpBounds.left - movementBounds.left) / - movementBounds.width(); - final float heightFraction = (float) (tmpBounds.top - movementBounds.top) / - movementBounds.height(); - if (tmpBounds.top == movementBounds.top) { - return widthFraction; - } else if (tmpBounds.left == movementBounds.right) { - return 1f + heightFraction; - } else if (tmpBounds.top == movementBounds.bottom) { - return 2f + (1f - widthFraction); - } else { - return 3f + (1f - heightFraction); - } - } - - /** - * Moves the {@param stackBounds} along the {@param movementBounds} to the given snap fraction. - * See {@link #getSnapFraction(Rect, Rect)}. - * - * The fraction is define in a clockwise fashion against the {@param movementBounds}: - * - * 0 1 - * 4 +---+ 1 - * | | - * 3 +---+ 2 - * 3 2 - */ - public void applySnapFraction(Rect stackBounds, Rect movementBounds, float snapFraction) { - if (snapFraction < 1f) { - int offset = movementBounds.left + (int) (snapFraction * movementBounds.width()); - stackBounds.offsetTo(offset, movementBounds.top); - } else if (snapFraction < 2f) { - snapFraction -= 1f; - int offset = movementBounds.top + (int) (snapFraction * movementBounds.height()); - stackBounds.offsetTo(movementBounds.right, offset); - } else if (snapFraction < 3f) { - snapFraction -= 2f; - int offset = movementBounds.left + (int) ((1f - snapFraction) * movementBounds.width()); - stackBounds.offsetTo(offset, movementBounds.bottom); - } else { - snapFraction -= 3f; - int offset = movementBounds.top + (int) ((1f - snapFraction) * movementBounds.height()); - stackBounds.offsetTo(movementBounds.left, offset); - } - } - - /** - * Adjusts {@param movementBoundsOut} so that it is the movement bounds for the given - * {@param stackBounds}. - */ - public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut, - int bottomOffset) { - // Adjust the right/bottom to ensure the stack bounds never goes offscreen - movementBoundsOut.set(insetBounds); - movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right - - stackBounds.width()); - movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom - - stackBounds.height()); - movementBoundsOut.bottom -= bottomOffset; - } - - /** - * @return the size of the PiP at the given {@param aspectRatio}, ensuring that the minimum edge - * is at least {@param minEdgeSize}. - */ - public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth, - int displayHeight) { - final int smallestDisplaySize = Math.min(displayWidth, displayHeight); - final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent); - - final int width; - final int height; - if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) { - // Beyond these points, we can just use the min size as the shorter edge - if (aspectRatio <= 1) { - // Portrait, width is the minimum size - width = minSize; - height = Math.round(width / aspectRatio); - } else { - // Landscape, height is the minimum size - height = minSize; - width = Math.round(height * aspectRatio); - } - } else { - // Within these points, we ensure that the bounds fit within the radius of the limits - // at the points - final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize; - final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize); - height = (int) Math.round(Math.sqrt((radius * radius) / - (aspectRatio * aspectRatio + 1))); - width = Math.round(height * aspectRatio); - } - return new Size(width, height); - } - - /** - * @return the adjusted size so that it conforms to the given aspectRatio, ensuring that the - * minimum edge is at least minEdgeSize. - */ - public Size getSizeForAspectRatio(Size size, float aspectRatio, float minEdgeSize) { - final int smallestSize = Math.min(size.getWidth(), size.getHeight()); - final int minSize = (int) Math.max(minEdgeSize, smallestSize); - - final int width; - final int height; - if (aspectRatio <= 1) { - // Portrait, width is the minimum size. - width = minSize; - height = Math.round(width / aspectRatio); - } else { - // Landscape, height is the minimum size - height = minSize; - width = Math.round(height * aspectRatio); - } - return new Size(width, height); - } - - /** - * Snaps the {@param stackBounds} to the closest edge of the {@param movementBounds} and writes - * the new bounds out to {@param boundsOut}. - */ - public void snapRectToClosestEdge(Rect stackBounds, Rect movementBounds, Rect boundsOut) { - final int boundedLeft = Math.max(movementBounds.left, Math.min(movementBounds.right, - stackBounds.left)); - final int boundedTop = Math.max(movementBounds.top, Math.min(movementBounds.bottom, - stackBounds.top)); - boundsOut.set(stackBounds); - - // Otherwise, just find the closest edge - final int fromLeft = Math.abs(stackBounds.left - movementBounds.left); - final int fromTop = Math.abs(stackBounds.top - movementBounds.top); - final int fromRight = Math.abs(movementBounds.right - stackBounds.left); - final int fromBottom = Math.abs(movementBounds.bottom - stackBounds.top); - final int shortest = Math.min(Math.min(fromLeft, fromRight), Math.min(fromTop, fromBottom)); - if (shortest == fromLeft) { - boundsOut.offsetTo(movementBounds.left, boundedTop); - } else if (shortest == fromTop) { - boundsOut.offsetTo(boundedLeft, movementBounds.top); - } else if (shortest == fromRight) { - boundsOut.offsetTo(movementBounds.right, boundedTop); - } else { - boundsOut.offsetTo(boundedLeft, movementBounds.bottom); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java deleted file mode 100644 index 3e98169c5b2b..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2020 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.pip; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.RectF; -import android.view.SurfaceControl; - -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.wm.shell.R; - -/** - * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition. - */ -@SysUISingleton -public class PipSurfaceTransactionHelper implements ConfigurationController.ConfigurationListener { - - private final Context mContext; - private final boolean mEnableCornerRadius; - private int mCornerRadius; - - /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */ - private final Matrix mTmpTransform = new Matrix(); - private final float[] mTmpFloat9 = new float[9]; - private final RectF mTmpSourceRectF = new RectF(); - private final RectF mTmpDestinationRectF = new RectF(); - private final Rect mTmpDestinationRect = new Rect(); - - public PipSurfaceTransactionHelper(Context context, ConfigurationController configController) { - final Resources res = context.getResources(); - mContext = context; - mEnableCornerRadius = res.getBoolean(R.bool.config_pipEnableRoundCorner); - configController.addCallback(this); - } - - @Override - public void onDensityOrFontScaleChanged() { - final Resources res = mContext.getResources(); - mCornerRadius = res.getDimensionPixelSize(R.dimen.pip_corner_radius); - } - - /** - * Operates the alpha on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash, - float alpha) { - tx.setAlpha(leash, alpha); - return this; - } - - /** - * Operates the crop (and position) on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect destinationBounds) { - tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) - .setPosition(leash, destinationBounds.left, destinationBounds.top); - return this; - } - - /** - * Operates the scale (setMatrix) on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, Rect destinationBounds) { - mTmpSourceRectF.set(sourceBounds); - mTmpDestinationRectF.set(destinationBounds); - mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); - tx.setMatrix(leash, mTmpTransform, mTmpFloat9) - .setPosition(leash, mTmpDestinationRectF.left, mTmpDestinationRectF.top); - return this; - } - - /** - * Operates the scale (setMatrix) on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, Rect destinationBounds, Rect insets) { - mTmpSourceRectF.set(sourceBounds); - mTmpDestinationRect.set(sourceBounds); - mTmpDestinationRect.inset(insets); - // Scale by the shortest edge and offset such that the top/left of the scaled inset source - // rect aligns with the top/left of the destination bounds - final float scale = sourceBounds.width() <= sourceBounds.height() - ? (float) destinationBounds.width() / sourceBounds.width() - : (float) destinationBounds.height() / sourceBounds.height(); - final float left = destinationBounds.left - insets.left * scale; - final float top = destinationBounds.top - insets.top * scale; - mTmpTransform.setScale(scale, scale); - tx.setMatrix(leash, mTmpTransform, mTmpFloat9) - .setWindowCrop(leash, mTmpDestinationRect) - .setPosition(leash, left, top); - return this; - } - - /** - * Resets the scale (setMatrix) on a given transaction and leash if there's any - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect destinationBounds) { - tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9) - .setPosition(leash, destinationBounds.left, destinationBounds.top); - return this; - } - - /** - * Operates the round corner radius on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash, - boolean applyCornerRadius) { - if (mEnableCornerRadius) { - tx.setCornerRadius(leash, applyCornerRadius ? mCornerRadius : 0); - } - return this; - } - - interface SurfaceControlTransactionFactory { - SurfaceControl.Transaction getTransaction(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java deleted file mode 100644 index b6334c562a02..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ /dev/null @@ -1,1091 +0,0 @@ -/* - * Copyright (C) 2020 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.pip; - -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - -import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_ALPHA; -import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_BOUNDS; -import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; -import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; -import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; -import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; -import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; -import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; -import static com.android.systemui.pip.PipAnimationController.isInPipDirection; -import static com.android.systemui.pip.PipAnimationController.isOutPipDirection; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.ActivityTaskManager; -import android.app.PictureInPictureParams; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.RemoteException; -import android.util.Log; -import android.util.Rational; -import android.util.Size; -import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.View; -import android.view.WindowManager; -import android.window.TaskOrganizer; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; -import android.window.WindowContainerTransactionCallback; - -import com.android.internal.os.SomeArgs; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.pip.phone.PipMenuActivityController; -import com.android.systemui.pip.phone.PipUpdateThread; -import com.android.wm.shell.R; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayController; -import com.android.wm.shell.splitscreen.SplitScreen; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Consumer; - -/** - * Manages PiP tasks such as resize and offset. - * - * This class listens on {@link TaskOrganizer} callbacks for windowing mode change - * both to and from PiP and issues corresponding animation if applicable. - * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running - * and files a final {@link WindowContainerTransaction} at the end of the transition. - * - * This class is also responsible for general resize/offset PiP operations within SysUI component, - * see also {@link com.android.systemui.pip.phone.PipMotionHelper}. - */ -@SysUISingleton -public class PipTaskOrganizer extends TaskOrganizer implements ShellTaskOrganizer.TaskListener, - DisplayController.OnDisplaysChangedListener { - private static final String TAG = PipTaskOrganizer.class.getSimpleName(); - private static final boolean DEBUG = false; - - private static final int MSG_RESIZE_IMMEDIATE = 1; - private static final int MSG_RESIZE_ANIMATE = 2; - private static final int MSG_OFFSET_ANIMATE = 3; - private static final int MSG_FINISH_RESIZE = 4; - private static final int MSG_RESIZE_USER = 5; - - // Not a complete set of states but serves what we want right now. - private enum State { - UNDEFINED(0), - TASK_APPEARED(1), - ENTERING_PIP(2), - EXITING_PIP(3); - - private final int mStateValue; - - State(int value) { - mStateValue = value; - } - - private boolean isInPip() { - return mStateValue >= TASK_APPEARED.mStateValue - && mStateValue != EXITING_PIP.mStateValue; - } - - /** - * Resize request can be initiated in other component, ignore if we are no longer in PIP, - * still waiting for animation or we're exiting from it. - * - * @return {@code true} if the resize request should be blocked/ignored. - */ - private boolean shouldBlockResizeRequest() { - return mStateValue < ENTERING_PIP.mStateValue - || mStateValue == EXITING_PIP.mStateValue; - } - } - - private final Context mContext; - private final Handler mMainHandler; - private final Handler mUpdateHandler; - private final PipBoundsHandler mPipBoundsHandler; - private final PipAnimationController mPipAnimationController; - private final PipUiEventLogger mPipUiEventLoggerLogger; - private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); - private final Rect mLastReportedBounds = new Rect(); - private final int mEnterExitAnimationDuration; - private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; - private final Map<IBinder, Configuration> mInitialState = new HashMap<>(); - private final Optional<SplitScreen> mSplitScreenOptional; - protected final ShellTaskOrganizer mTaskOrganizer; - private SurfaceControlViewHost mPipViewHost; - private SurfaceControl mPipMenuSurface; - - // These callbacks are called on the update thread - private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = - new PipAnimationController.PipAnimationCallback() { - @Override - public void onPipAnimationStart(PipAnimationController.PipTransitionAnimator animator) { - sendOnPipTransitionStarted(animator.getTransitionDirection()); - } - - @Override - public void onPipAnimationEnd(SurfaceControl.Transaction tx, - PipAnimationController.PipTransitionAnimator animator) { - finishResize(tx, animator.getDestinationBounds(), animator.getTransitionDirection(), - animator.getAnimationType()); - sendOnPipTransitionFinished(animator.getTransitionDirection()); - } - - @Override - public void onPipAnimationCancel(PipAnimationController.PipTransitionAnimator animator) { - sendOnPipTransitionCancelled(animator.getTransitionDirection()); - } - }; - - @SuppressWarnings("unchecked") - private final Handler.Callback mUpdateCallbacks = (msg) -> { - SomeArgs args = (SomeArgs) msg.obj; - Consumer<Rect> updateBoundsCallback = (Consumer<Rect>) args.arg1; - switch (msg.what) { - case MSG_RESIZE_IMMEDIATE: { - Rect toBounds = (Rect) args.arg2; - resizePip(toBounds); - if (updateBoundsCallback != null) { - updateBoundsCallback.accept(toBounds); - } - break; - } - case MSG_RESIZE_ANIMATE: { - Rect currentBounds = (Rect) args.arg2; - Rect toBounds = (Rect) args.arg3; - Rect sourceHintRect = (Rect) args.arg4; - int duration = args.argi2; - animateResizePip(currentBounds, toBounds, sourceHintRect, - args.argi1 /* direction */, duration); - if (updateBoundsCallback != null) { - updateBoundsCallback.accept(toBounds); - } - break; - } - case MSG_OFFSET_ANIMATE: { - Rect originalBounds = (Rect) args.arg2; - final int offset = args.argi1; - final int duration = args.argi2; - offsetPip(originalBounds, 0 /* xOffset */, offset, duration); - Rect toBounds = new Rect(originalBounds); - toBounds.offset(0, offset); - if (updateBoundsCallback != null) { - updateBoundsCallback.accept(toBounds); - } - break; - } - case MSG_FINISH_RESIZE: { - SurfaceControl.Transaction tx = (SurfaceControl.Transaction) args.arg2; - Rect toBounds = (Rect) args.arg3; - finishResize(tx, toBounds, args.argi1 /* direction */, -1); - if (updateBoundsCallback != null) { - updateBoundsCallback.accept(toBounds); - } - break; - } - case MSG_RESIZE_USER: { - Rect startBounds = (Rect) args.arg2; - Rect toBounds = (Rect) args.arg3; - userResizePip(startBounds, toBounds); - if (updateBoundsCallback != null) { - updateBoundsCallback.accept(toBounds); - } - break; - } - } - args.recycle(); - return true; - }; - - private ActivityManager.RunningTaskInfo mTaskInfo; - private WindowContainerToken mToken; - private SurfaceControl mLeash; - private State mState = State.UNDEFINED; - private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; - private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory - mSurfaceControlTransactionFactory; - private PictureInPictureParams mPictureInPictureParams; - - /** - * If set to {@code true}, the entering animation will be skipped and we will wait for - * {@link #onFixedRotationFinished(int)} callback to actually enter PiP. - */ - private boolean mShouldDeferEnteringPip; - - public PipTaskOrganizer(Context context, @NonNull PipBoundsHandler boundsHandler, - @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, - Optional<SplitScreen> splitScreenOptional, - @NonNull DisplayController displayController, - @NonNull PipUiEventLogger pipUiEventLogger, - @NonNull ShellTaskOrganizer shellTaskOrganizer) { - mContext = context; - mMainHandler = new Handler(Looper.getMainLooper()); - mUpdateHandler = new Handler(PipUpdateThread.get().getLooper(), mUpdateCallbacks); - mPipBoundsHandler = boundsHandler; - mEnterExitAnimationDuration = context.getResources() - .getInteger(R.integer.config_pipResizeAnimationDuration); - mSurfaceTransactionHelper = surfaceTransactionHelper; - mPipAnimationController = new PipAnimationController(mSurfaceTransactionHelper); - mPipUiEventLoggerLogger = pipUiEventLogger; - mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; - mSplitScreenOptional = splitScreenOptional; - mTaskOrganizer = shellTaskOrganizer; - mTaskOrganizer.addListener(this, WINDOWING_MODE_PINNED); - displayController.addDisplayWindowListener(this); - } - - public Handler getUpdateHandler() { - return mUpdateHandler; - } - - public Rect getLastReportedBounds() { - return new Rect(mLastReportedBounds); - } - - public Rect getCurrentOrAnimatingBounds() { - PipAnimationController.PipTransitionAnimator animator = - mPipAnimationController.getCurrentAnimator(); - if (animator != null && animator.isRunning()) { - return new Rect(animator.getDestinationBounds()); - } - return getLastReportedBounds(); - } - - public boolean isInPip() { - return mState.isInPip(); - } - - public boolean isDeferringEnterPipAnimation() { - return mState.isInPip() && mShouldDeferEnteringPip; - } - - /** - * Registers {@link PipTransitionCallback} to receive transition callbacks. - */ - public void registerPipTransitionCallback(PipTransitionCallback callback) { - mPipTransitionCallbacks.add(callback); - } - - /** - * Sets the preferred animation type for one time. - * This is typically used to set the animation type to - * {@link PipAnimationController#ANIM_TYPE_ALPHA}. - */ - public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) { - mOneShotAnimationType = animationType; - } - - /** - * Expands PiP to the previous bounds, this is done in two phases using - * {@link WindowContainerTransaction} - * - setActivityWindowingMode to either fullscreen or split-secondary at beginning of the - * transaction. without changing the windowing mode of the Task itself. This makes sure the - * activity render it's final configuration while the Task is still in PiP. - * - setWindowingMode to undefined at the end of transition - * @param animationDurationMs duration in millisecond for the exiting PiP transition - */ - public void exitPip(int animationDurationMs) { - if (!mState.isInPip() || mState == State.EXITING_PIP || mToken == null) { - Log.wtf(TAG, "Not allowed to exitPip in current state" - + " mState=" + mState + " mToken=" + mToken); - return; - } - - mPipUiEventLoggerLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); - final Configuration initialConfig = mInitialState.remove(mToken.asBinder()); - final boolean orientationDiffers = initialConfig.windowConfiguration.getRotation() - != mPipBoundsHandler.getDisplayRotation(); - final WindowContainerTransaction wct = new WindowContainerTransaction(); - final Rect destinationBounds = initialConfig.windowConfiguration.getBounds(); - final int direction = syncWithSplitScreenBounds(destinationBounds) - ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN - : TRANSITION_DIRECTION_LEAVE_PIP; - if (orientationDiffers) { - mState = State.EXITING_PIP; - // Send started callback though animation is ignored. - sendOnPipTransitionStarted(direction); - // Don't bother doing an animation if the display rotation differs or if it's in - // a non-supported windowing mode - applyWindowingModeChangeOnExit(wct, direction); - mTaskOrganizer.applyTransaction(wct); - // Send finished callback though animation is ignored. - sendOnPipTransitionFinished(direction); - } else { - final SurfaceControl.Transaction tx = - mSurfaceControlTransactionFactory.getTransaction(); - mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, - mLastReportedBounds); - tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); - // We set to fullscreen here for now, but later it will be set to UNDEFINED for - // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. - wct.setActivityWindowingMode(mToken, - direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN - ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY - : WINDOWING_MODE_FULLSCREEN); - wct.setBounds(mToken, destinationBounds); - wct.setBoundsChangeTransaction(mToken, tx); - mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() { - @Override - public void onTransactionReady(int id, SurfaceControl.Transaction t) { - t.apply(); - scheduleAnimateResizePip(mLastReportedBounds, destinationBounds, - getValidSourceHintRect(mTaskInfo, destinationBounds), direction, - animationDurationMs, null /* updateBoundsCallback */); - mState = State.EXITING_PIP; - } - }); - } - } - - private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { - // Reset the final windowing mode. - wct.setWindowingMode(mToken, getOutPipWindowingMode()); - // Simply reset the activity mode set prior to the animation running. - wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); - mSplitScreenOptional.ifPresent(splitScreen -> { - if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { - wct.reparent(mToken, splitScreen.getSecondaryRoot(), true /* onTop */); - } - }); - } - - /** - * Removes PiP immediately. - */ - public void removePip() { - if (!mState.isInPip() || mToken == null) { - Log.wtf(TAG, "Not allowed to removePip in current state" - + " mState=" + mState + " mToken=" + mToken); - return; - } - - // removePipImmediately is expected when the following animation finishes. - mUpdateHandler.post(() -> mPipAnimationController - .getAnimator(mLeash, mLastReportedBounds, 1f, 0f) - .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK) - .setPipAnimationCallback(mPipAnimationCallback) - .setDuration(mEnterExitAnimationDuration) - .start()); - mInitialState.remove(mToken.asBinder()); - mState = State.EXITING_PIP; - } - - private void removePipImmediately() { - try { - // Reset the task bounds first to ensure the activity configuration is reset as well - final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mToken, null); - mTaskOrganizer.applyTransaction(wct); - - ActivityTaskManager.getService().removeStacksInWindowingModes( - new int[]{ WINDOWING_MODE_PINNED }); - } catch (RemoteException e) { - Log.e(TAG, "Failed to remove PiP", e); - } - } - - @Override - public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { - Objects.requireNonNull(info, "Requires RunningTaskInfo"); - mTaskInfo = info; - mToken = mTaskInfo.token; - mState = State.TASK_APPEARED; - mLeash = leash; - mInitialState.put(mToken.asBinder(), new Configuration(mTaskInfo.configuration)); - mPictureInPictureParams = mTaskInfo.pictureInPictureParams; - - mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); - mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); - - if (mShouldDeferEnteringPip) { - if (DEBUG) Log.d(TAG, "Defer entering PiP animation, fixed rotation is ongoing"); - // if deferred, hide the surface till fixed rotation is completed - final SurfaceControl.Transaction tx = - mSurfaceControlTransactionFactory.getTransaction(); - tx.setAlpha(mLeash, 0f); - tx.show(mLeash); - tx.apply(); - return; - } - - final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( - mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), - null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); - Objects.requireNonNull(destinationBounds, "Missing destination bounds"); - final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); - - if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { - final Rect sourceHintRect = getValidSourceHintRect(info, currentBounds); - scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect, - TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration, - null /* updateBoundsCallback */); - mState = State.ENTERING_PIP; - } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { - enterPipWithAlphaAnimation(destinationBounds, mEnterExitAnimationDuration); - mOneShotAnimationType = ANIM_TYPE_BOUNDS; - } else { - throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); - } - } - - /** - * Returns the source hint rect if it is valid (if provided and is contained by the current - * task bounds). - */ - private Rect getValidSourceHintRect(ActivityManager.RunningTaskInfo info, Rect sourceBounds) { - final Rect sourceHintRect = info.pictureInPictureParams != null - && info.pictureInPictureParams.hasSourceBoundsHint() - ? info.pictureInPictureParams.getSourceRectHint() - : null; - if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) { - return sourceHintRect; - } - return null; - } - - private void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { - // If we are fading the PIP in, then we should move the pip to the final location as - // soon as possible, but set the alpha immediately since the transaction can take a - // while to process - final SurfaceControl.Transaction tx = - mSurfaceControlTransactionFactory.getTransaction(); - tx.setAlpha(mLeash, 0f); - tx.apply(); - final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); - wct.setBounds(mToken, destinationBounds); - wct.scheduleFinishEnterPip(mToken, destinationBounds); - mTaskOrganizer.applySyncTransaction(wct, new WindowContainerTransactionCallback() { - @Override - public void onTransactionReady(int id, SurfaceControl.Transaction t) { - t.apply(); - mUpdateHandler.post(() -> mPipAnimationController - .getAnimator(mLeash, destinationBounds, 0f, 1f) - .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) - .setPipAnimationCallback(mPipAnimationCallback) - .setDuration(durationMs) - .start()); - // mState is set right after the animation is kicked off to block any resize - // requests such as offsetPip that may have been called prior to the transition. - mState = State.ENTERING_PIP; - } - }); - } - - private void sendOnPipTransitionStarted( - @PipAnimationController.TransitionDirection int direction) { - final Rect pipBounds = new Rect(mLastReportedBounds); - runOnMainHandler(() -> { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionStarted(mTaskInfo.baseActivity, direction, pipBounds); - } - }); - } - - private void sendOnPipTransitionFinished( - @PipAnimationController.TransitionDirection int direction) { - runOnMainHandler(() -> { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionFinished(mTaskInfo.baseActivity, direction); - } - }); - } - - private void sendOnPipTransitionCancelled( - @PipAnimationController.TransitionDirection int direction) { - runOnMainHandler(() -> { - for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { - final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); - callback.onPipTransitionCanceled(mTaskInfo.baseActivity, direction); - } - }); - } - - private void runOnMainHandler(Runnable r) { - if (Looper.getMainLooper() == Looper.myLooper()) { - r.run(); - } else { - mMainHandler.post(r); - } - } - - /** - * Setup the ViewHost and attach the provided menu view to the ViewHost. - */ - public void attachPipMenuViewHost(View menuView, WindowManager.LayoutParams lp) { - if (mPipMenuSurface != null) { - Log.e(TAG, "PIP Menu View already created and attached."); - return; - } - - if (mLeash == null) { - Log.e(TAG, "PiP Leash is not yet ready."); - return; - } - - if (Looper.getMainLooper() != Looper.myLooper()) { - throw new RuntimeException("PipMenuView needs to be attached on the main thread."); - } - - mPipViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), - (android.os.Binder) null); - mPipMenuSurface = mPipViewHost.getSurfacePackage().getSurfaceControl(); - SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); - transaction.reparent(mPipMenuSurface, mLeash); - transaction.show(mPipMenuSurface); - transaction.setRelativeLayer(mPipMenuSurface, mLeash, 1); - transaction.apply(); - mPipViewHost.setView(menuView, lp); - } - - - /** - * Releases the PIP Menu's View host, remove it from PIP task surface. - */ - public void detachPipMenuViewHost() { - if (mPipMenuSurface != null) { - SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); - transaction.remove(mPipMenuSurface); - transaction.apply(); - mPipMenuSurface = null; - mPipViewHost = null; - } - } - - /** - * Return whether the PiP Menu has been attached to the leash yet. - */ - public boolean isPipMenuViewHostAttached() { - return mPipViewHost != null; - } - - - /** - * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int)}. - * Meanwhile this callback is invoked whenever the task is removed. For instance: - * - as a result of removeStacksInWindowingModes from WM - * - activity itself is died - * Nevertheless, we simply update the internal state here as all the heavy lifting should - * have been done in WM. - */ - @Override - public void onTaskVanished(ActivityManager.RunningTaskInfo info) { - if (!mState.isInPip()) { - return; - } - final WindowContainerToken token = info.token; - Objects.requireNonNull(token, "Requires valid WindowContainerToken"); - if (token.asBinder() != mToken.asBinder()) { - Log.wtf(TAG, "Unrecognized token: " + token); - return; - } - mShouldDeferEnteringPip = false; - mPictureInPictureParams = null; - mState = State.UNDEFINED; - mPipUiEventLoggerLogger.setTaskInfo(null); - } - - @Override - public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { - Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); - final PictureInPictureParams newParams = info.pictureInPictureParams; - if (newParams == null || !applyPictureInPictureParams(newParams)) { - Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); - return; - } - final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( - info.topActivity, getAspectRatioOrDefault(newParams), - mLastReportedBounds, getMinimalSize(info.topActivityInfo), - true /* userCurrentMinEdgeSize */); - Objects.requireNonNull(destinationBounds, "Missing destination bounds"); - scheduleAnimateResizePip(destinationBounds, mEnterExitAnimationDuration, - null /* updateBoundsCallback */); - } - - @Override - public void onFixedRotationStarted(int displayId, int newRotation) { - mShouldDeferEnteringPip = true; - } - - @Override - public void onFixedRotationFinished(int displayId) { - if (mShouldDeferEnteringPip && mState.isInPip()) { - final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( - mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), - null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); - // schedule a regular animation to ensure all the callbacks are still being sent - enterPipWithAlphaAnimation(destinationBounds, 0 /* durationMs */); - } - mShouldDeferEnteringPip = false; - } - - /** - * TODO(b/152809058): consolidate the display info handling logic in SysUI - * - * @param destinationBoundsOut the current destination bounds will be populated to this param - */ - @SuppressWarnings("unchecked") - public void onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, - boolean fromImeAdjustment, boolean fromShelfAdjustment, - WindowContainerTransaction wct) { - final PipAnimationController.PipTransitionAnimator animator = - mPipAnimationController.getCurrentAnimator(); - if (animator == null || !animator.isRunning() - || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { - if (mState.isInPip() && fromRotation) { - // If we are rotating while there is a current animation, immediately cancel the - // animation (remove the listeners so we don't trigger the normal finish resize - // call that should only happen on the update thread) - int direction = TRANSITION_DIRECTION_NONE; - if (animator != null) { - direction = animator.getTransitionDirection(); - animator.removeAllUpdateListeners(); - animator.removeAllListeners(); - animator.cancel(); - // Do notify the listeners that this was canceled - sendOnPipTransitionCancelled(direction); - sendOnPipTransitionFinished(direction); - } - mLastReportedBounds.set(destinationBoundsOut); - - // Create a reset surface transaction for the new bounds and update the window - // container transaction - final SurfaceControl.Transaction tx = createFinishResizeSurfaceTransaction( - destinationBoundsOut); - prepareFinishResizeTransaction(destinationBoundsOut, direction, tx, wct); - } else { - // There could be an animation on-going. If there is one on-going, last-reported - // bounds isn't yet updated. We'll use the animator's bounds instead. - if (animator != null && animator.isRunning()) { - if (!animator.getDestinationBounds().isEmpty()) { - destinationBoundsOut.set(animator.getDestinationBounds()); - } - } else { - if (!mLastReportedBounds.isEmpty()) { - destinationBoundsOut.set(mLastReportedBounds); - } - } - } - return; - } - - final Rect currentDestinationBounds = animator.getDestinationBounds(); - destinationBoundsOut.set(currentDestinationBounds); - if (!fromImeAdjustment && !fromShelfAdjustment - && mPipBoundsHandler.getDisplayBounds().contains(currentDestinationBounds)) { - // no need to update the destination bounds, bail early - return; - } - - final Rect newDestinationBounds = mPipBoundsHandler.getDestinationBounds( - mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), - null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); - if (newDestinationBounds.equals(currentDestinationBounds)) return; - if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { - animator.updateEndValue(newDestinationBounds); - } - animator.setDestinationBounds(newDestinationBounds); - destinationBoundsOut.set(newDestinationBounds); - } - - /** - * @return {@code true} if the aspect ratio is changed since no other parameters within - * {@link PictureInPictureParams} would affect the bounds. - */ - private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) { - final Rational currentAspectRatio = - mPictureInPictureParams != null ? mPictureInPictureParams.getAspectRatioRational() - : null; - final boolean aspectRatioChanged = !Objects.equals(currentAspectRatio, - params.getAspectRatioRational()); - mPictureInPictureParams = params; - if (aspectRatioChanged) { - mPipBoundsHandler.onAspectRatioChanged(params.getAspectRatio()); - } - return aspectRatioChanged; - } - - /** - * Animates resizing of the pinned stack given the duration. - */ - public void scheduleAnimateResizePip(Rect toBounds, int duration, - Consumer<Rect> updateBoundsCallback) { - if (mShouldDeferEnteringPip) { - Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); - return; - } - scheduleAnimateResizePip(mLastReportedBounds, toBounds, null /* sourceHintRect */, - TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback); - } - - private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, - Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, - int durationMs, Consumer<Rect> updateBoundsCallback) { - if (!mState.isInPip()) { - // TODO: tend to use shouldBlockResizeRequest here as well but need to consider - // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window - // container transaction callback and we want to set the mState immediately. - return; - } - - SomeArgs args = SomeArgs.obtain(); - args.arg1 = updateBoundsCallback; - args.arg2 = currentBounds; - args.arg3 = destinationBounds; - args.arg4 = sourceHintRect; - args.argi1 = direction; - args.argi2 = durationMs; - mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args)); - } - - /** - * Directly perform manipulation/resize on the leash. This will not perform any - * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. - */ - public void scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback) { - SomeArgs args = SomeArgs.obtain(); - args.arg1 = updateBoundsCallback; - args.arg2 = toBounds; - mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_IMMEDIATE, args)); - } - - /** - * Directly perform a scaled matrix transformation on the leash. This will not perform any - * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. - */ - public void scheduleUserResizePip(Rect startBounds, Rect toBounds, - Consumer<Rect> updateBoundsCallback) { - SomeArgs args = SomeArgs.obtain(); - args.arg1 = updateBoundsCallback; - args.arg2 = startBounds; - args.arg3 = toBounds; - mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_USER, args)); - } - - /** - * Finish an intermediate resize operation. This is expected to be called after - * {@link #scheduleResizePip}. - */ - public void scheduleFinishResizePip(Rect destinationBounds) { - scheduleFinishResizePip(destinationBounds, null /* updateBoundsCallback */); - } - - /** - * Same as {@link #scheduleFinishResizePip} but with a callback. - */ - public void scheduleFinishResizePip(Rect destinationBounds, - Consumer<Rect> updateBoundsCallback) { - scheduleFinishResizePip(destinationBounds, TRANSITION_DIRECTION_NONE, updateBoundsCallback); - } - - private void scheduleFinishResizePip(Rect destinationBounds, - @PipAnimationController.TransitionDirection int direction, - Consumer<Rect> updateBoundsCallback) { - if (mState.shouldBlockResizeRequest()) { - return; - } - - SomeArgs args = SomeArgs.obtain(); - args.arg1 = updateBoundsCallback; - args.arg2 = createFinishResizeSurfaceTransaction( - destinationBounds); - args.arg3 = destinationBounds; - args.argi1 = direction; - mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_FINISH_RESIZE, args)); - } - - private SurfaceControl.Transaction createFinishResizeSurfaceTransaction( - Rect destinationBounds) { - final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - mSurfaceTransactionHelper - .crop(tx, mLeash, destinationBounds) - .resetScale(tx, mLeash, destinationBounds) - .round(tx, mLeash, mState.isInPip()); - return tx; - } - - /** - * Offset the PiP window by a given offset on Y-axis, triggered also from screen rotation. - */ - public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, - Consumer<Rect> updateBoundsCallback) { - if (mState.shouldBlockResizeRequest()) { - return; - } - if (mShouldDeferEnteringPip) { - Log.d(TAG, "skip scheduleOffsetPip, entering pip deferred"); - return; - } - SomeArgs args = SomeArgs.obtain(); - args.arg1 = updateBoundsCallback; - args.arg2 = originalBounds; - // offset would be zero if triggered from screen rotation. - args.argi1 = offset; - args.argi2 = duration; - mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_ANIMATE, args)); - } - - private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { - if (Looper.myLooper() != mUpdateHandler.getLooper()) { - throw new RuntimeException("Callers should call scheduleOffsetPip() instead of this " - + "directly"); - } - if (mTaskInfo == null) { - Log.w(TAG, "mTaskInfo is not set"); - return; - } - final Rect destinationBounds = new Rect(originalBounds); - destinationBounds.offset(xOffset, yOffset); - animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, - TRANSITION_DIRECTION_SAME, durationMs); - } - - private void resizePip(Rect destinationBounds) { - if (Looper.myLooper() != mUpdateHandler.getLooper()) { - throw new RuntimeException("Callers should call scheduleResizePip() instead of this " - + "directly"); - } - // Could happen when exitPip - if (mToken == null || mLeash == null) { - Log.w(TAG, "Abort animation, invalid leash"); - return; - } - mLastReportedBounds.set(destinationBounds); - final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - mSurfaceTransactionHelper - .crop(tx, mLeash, destinationBounds) - .round(tx, mLeash, mState.isInPip()); - tx.apply(); - } - - private void userResizePip(Rect startBounds, Rect destinationBounds) { - if (Looper.myLooper() != mUpdateHandler.getLooper()) { - throw new RuntimeException("Callers should call scheduleUserResizePip() instead of " - + "this directly"); - } - // Could happen when exitPip - if (mToken == null || mLeash == null) { - Log.w(TAG, "Abort animation, invalid leash"); - return; - } - - if (startBounds.isEmpty() || destinationBounds.isEmpty()) { - Log.w(TAG, "Attempted to user resize PIP to or from empty bounds, aborting."); - return; - } - - final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - mSurfaceTransactionHelper.scale(tx, mLeash, startBounds, destinationBounds); - tx.apply(); - } - - private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, - @PipAnimationController.TransitionDirection int direction, - @PipAnimationController.AnimationType int type) { - if (Looper.myLooper() != mUpdateHandler.getLooper()) { - throw new RuntimeException("Callers should call scheduleResizePip() instead of this " - + "directly"); - } - mLastReportedBounds.set(destinationBounds); - if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { - removePipImmediately(); - return; - } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { - return; - } - - WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareFinishResizeTransaction(destinationBounds, direction, tx, wct); - applyFinishBoundsResize(wct, direction); - runOnMainHandler(() -> { - if (mPipViewHost != null) { - mPipViewHost.relayout(PipMenuActivityController.getPipMenuLayoutParams( - destinationBounds.width(), destinationBounds.height())); - } - }); - } - - private void prepareFinishResizeTransaction(Rect destinationBounds, - @PipAnimationController.TransitionDirection int direction, - SurfaceControl.Transaction tx, - WindowContainerTransaction wct) { - final Rect taskBounds; - if (isInPipDirection(direction)) { - // If we are animating from fullscreen using a bounds animation, then reset the - // activity windowing mode set by WM, and set the task bounds to the final bounds - taskBounds = destinationBounds; - wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); - wct.scheduleFinishEnterPip(mToken, destinationBounds); - } else if (isOutPipDirection(direction)) { - // If we are animating to fullscreen, then we need to reset the override bounds - // on the task to ensure that the task "matches" the parent's bounds. - taskBounds = (direction == TRANSITION_DIRECTION_LEAVE_PIP) - ? null : destinationBounds; - applyWindowingModeChangeOnExit(wct, direction); - } else { - // Just a resize in PIP - taskBounds = destinationBounds; - } - - wct.setBounds(mToken, taskBounds); - wct.setBoundsChangeTransaction(mToken, tx); - } - - /** - * Applies the window container transaction to finish a bounds resize. - * - * Called by {@link #finishResize(SurfaceControl.Transaction, Rect, int, int)}} once it has - * finished preparing the transaction. It allows subclasses to modify the transaction before - * applying it. - */ - public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct, - @PipAnimationController.TransitionDirection int direction) { - mTaskOrganizer.applyTransaction(wct); - } - - /** - * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined - * and can be overridden to restore to an alternate windowing mode. - */ - public int getOutPipWindowingMode() { - // By default, simply reset the windowing mode to undefined. - return WINDOWING_MODE_UNDEFINED; - } - - - private void animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, - @PipAnimationController.TransitionDirection int direction, int durationMs) { - if (Looper.myLooper() != mUpdateHandler.getLooper()) { - throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of " - + "this directly"); - } - // Could happen when exitPip - if (mToken == null || mLeash == null) { - Log.w(TAG, "Abort animation, invalid leash"); - return; - } - mPipAnimationController - .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect, direction) - .setTransitionDirection(direction) - .setPipAnimationCallback(mPipAnimationCallback) - .setDuration(durationMs) - .start(); - } - - private Size getMinimalSize(ActivityInfo activityInfo) { - if (activityInfo == null || activityInfo.windowLayout == null) { - return null; - } - final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; - // -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout> - // without minWidth/minHeight - if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) { - return new Size(windowLayout.minWidth, windowLayout.minHeight); - } - return null; - } - - private float getAspectRatioOrDefault(@Nullable PictureInPictureParams params) { - return params == null || !params.hasSetAspectRatio() - ? mPipBoundsHandler.getDefaultAspectRatio() - : params.getAspectRatio(); - } - - /** - * Sync with {@link SplitScreen} on destination bounds if PiP is going to split screen. - * - * @param destinationBoundsOut contain the updated destination bounds if applicable - * @return {@code true} if destinationBounds is altered for split screen - */ - private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut) { - if (!mSplitScreenOptional.isPresent()) { - return false; - } - - SplitScreen splitScreen = mSplitScreenOptional.get(); - if (!splitScreen.isDividerVisible()) { - // fail early if system is not in split screen mode - return false; - } - - // PiP window will go to split-secondary mode instead of fullscreen, populates the - // split screen bounds here. - destinationBoundsOut.set(splitScreen.getDividerView() - .getNonMinimizedSplitScreenSecondaryBounds()); - return true; - } - - /** - * Dumps internal states. - */ - public void dump(PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.println(prefix + TAG); - pw.println(innerPrefix + "mTaskInfo=" + mTaskInfo); - pw.println(innerPrefix + "mToken=" + mToken - + " binder=" + (mToken != null ? mToken.asBinder() : null)); - pw.println(innerPrefix + "mLeash=" + mLeash); - pw.println(innerPrefix + "mState=" + mState); - pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); - pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); - pw.println(innerPrefix + "mLastReportedBounds=" + mLastReportedBounds); - pw.println(innerPrefix + "mInitialState:"); - for (Map.Entry<IBinder, Configuration> e : mInitialState.entrySet()) { - pw.println(innerPrefix + " binder=" + e.getKey() - + " winConfig=" + e.getValue().windowConfiguration); - } - } - - /** - * Callback interface for PiP transitions (both from and to PiP mode) - */ - public interface PipTransitionCallback { - /** - * Callback when the pip transition is started. - */ - void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds); - - /** - * Callback when the pip transition is finished. - */ - void onPipTransitionFinished(ComponentName activity, int direction); - - /** - * Callback when the pip transition is cancelled. - */ - void onPipTransitionCanceled(ComponentName activity, int direction); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipUiEventLogger.java b/packages/SystemUI/src/com/android/systemui/pip/PipUiEventLogger.java deleted file mode 100644 index 22adbb77d70a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/PipUiEventLogger.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2020 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.pip; - -import android.app.TaskInfo; -import android.content.pm.PackageManager; - -import com.android.internal.logging.UiEvent; -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.dagger.SysUISingleton; - -/** - * Helper class that ends PiP log to UiEvent, see also go/uievent - */ -@SysUISingleton -public class PipUiEventLogger { - - private static final int INVALID_PACKAGE_UID = -1; - - private final UiEventLogger mUiEventLogger; - private final PackageManager mPackageManager; - - private String mPackageName; - private int mPackageUid = INVALID_PACKAGE_UID; - - public PipUiEventLogger(UiEventLogger uiEventLogger, PackageManager packageManager) { - mUiEventLogger = uiEventLogger; - mPackageManager = packageManager; - } - - public void setTaskInfo(TaskInfo taskInfo) { - if (taskInfo == null) { - mPackageName = null; - mPackageUid = INVALID_PACKAGE_UID; - } else { - mPackageName = taskInfo.topActivity.getPackageName(); - mPackageUid = getUid(mPackageName, taskInfo.userId); - } - } - - /** - * Sends log via UiEvent, reference go/uievent for how to debug locally - */ - public void log(PipUiEventEnum event) { - if (mPackageName == null || mPackageUid == INVALID_PACKAGE_UID) { - return; - } - mUiEventLogger.log(event, mPackageUid, mPackageName); - } - - private int getUid(String packageName, int userId) { - int uid = INVALID_PACKAGE_UID; - try { - uid = mPackageManager.getApplicationInfoAsUser( - packageName, 0 /* ApplicationInfoFlags */, userId).uid; - } catch (PackageManager.NameNotFoundException e) { - // do nothing. - } - return uid; - } - - /** - * Enums for logging the PiP events to UiEvent - */ - public enum PipUiEventEnum implements UiEventLogger.UiEventEnum { - @UiEvent(doc = "Activity enters picture-in-picture mode") - PICTURE_IN_PICTURE_ENTER(603), - - @UiEvent(doc = "Expands from picture-in-picture to fullscreen") - PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN(604), - - @UiEvent(doc = "Removes picture-in-picture by tap close button") - PICTURE_IN_PICTURE_TAP_TO_REMOVE(605), - - @UiEvent(doc = "Removes picture-in-picture by drag to dismiss area") - PICTURE_IN_PICTURE_DRAG_TO_REMOVE(606), - - @UiEvent(doc = "Shows picture-in-picture menu") - PICTURE_IN_PICTURE_SHOW_MENU(607), - - @UiEvent(doc = "Hides picture-in-picture menu") - PICTURE_IN_PICTURE_HIDE_MENU(608), - - @UiEvent(doc = "Changes the aspect ratio of picture-in-picture window. This is inherited" - + " from previous Tron-based logging and currently not in use.") - PICTURE_IN_PICTURE_CHANGE_ASPECT_RATIO(609), - - @UiEvent(doc = "User resize of the picture-in-picture window") - PICTURE_IN_PICTURE_RESIZE(610); - - private final int mId; - - PipUiEventEnum(int id) { - mId = id; - } - - @Override - public int getId() { - return mId; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipAccessibilityInteractionConnection.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipAccessibilityInteractionConnection.java deleted file mode 100644 index a13318990f40..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipAccessibilityInteractionConnection.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright (C) 2017 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.pip.phone; - -import android.content.Context; -import android.graphics.Rect; -import android.graphics.Region; -import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteException; -import android.view.MagnificationSpec; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityWindowInfo; -import android.view.accessibility.IAccessibilityInteractionConnection; -import android.view.accessibility.IAccessibilityInteractionConnectionCallback; - -import com.android.systemui.pip.PipSnapAlgorithm; -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.wm.shell.R; - -import java.util.ArrayList; -import java.util.List; - -/** - * Expose the touch actions to accessibility as if this object were a window with a single view. - * That pseudo-view exposes all of the actions this object can perform. - */ -public class PipAccessibilityInteractionConnection - extends IAccessibilityInteractionConnection.Stub { - - public interface AccessibilityCallbacks { - void onAccessibilityShowMenu(); - } - - private static final long ACCESSIBILITY_NODE_ID = 1; - private List<AccessibilityNodeInfo> mAccessibilityNodeInfoList; - - private Context mContext; - private Handler mHandler; - private PipMotionHelper mMotionHelper; - private PipTaskOrganizer mTaskOrganizer; - private PipSnapAlgorithm mSnapAlgorithm; - private Runnable mUpdateMovementBoundCallback; - private AccessibilityCallbacks mCallbacks; - - private final Rect mNormalBounds = new Rect(); - private final Rect mExpandedBounds = new Rect(); - private final Rect mNormalMovementBounds = new Rect(); - private final Rect mExpandedMovementBounds = new Rect(); - private Rect mTmpBounds = new Rect(); - - public PipAccessibilityInteractionConnection(Context context, PipMotionHelper motionHelper, - PipTaskOrganizer taskOrganizer, PipSnapAlgorithm snapAlgorithm, - AccessibilityCallbacks callbacks, Runnable updateMovementBoundCallback, - Handler handler) { - mContext = context; - mHandler = handler; - mMotionHelper = motionHelper; - mTaskOrganizer = taskOrganizer; - mSnapAlgorithm = snapAlgorithm; - mUpdateMovementBoundCallback = updateMovementBoundCallback; - mCallbacks = callbacks; - } - - @Override - public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, - Region interactiveRegion, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args) { - try { - callback.setFindAccessibilityNodeInfosResult( - (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) - ? getNodeList() : null, interactionId); - } catch (RemoteException re) { - /* best effort - ignore */ - } - } - - @Override - public void performAccessibilityAction(long accessibilityNodeId, int action, - Bundle arguments, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid) { - // We only support one view. A request for anything else is invalid - boolean result = false; - if (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) { - - // R constants are not final so this cannot be put in the switch-case. - if (action == R.id.action_pip_resize) { - if (mMotionHelper.getBounds().width() == mNormalBounds.width() - && mMotionHelper.getBounds().height() == mNormalBounds.height()) { - setToExpandedBounds(); - } else { - setToNormalBounds(); - } - result = true; - } else { - switch (action) { - case AccessibilityNodeInfo.ACTION_CLICK: - mHandler.post(() -> { - mCallbacks.onAccessibilityShowMenu(); - }); - result = true; - break; - case AccessibilityNodeInfo.ACTION_DISMISS: - mMotionHelper.dismissPip(); - result = true; - break; - case com.android.internal.R.id.accessibilityActionMoveWindow: - int newX = arguments.getInt( - AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_X); - int newY = arguments.getInt( - AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_Y); - Rect pipBounds = new Rect(); - pipBounds.set(mMotionHelper.getBounds()); - mTmpBounds.offsetTo(newX, newY); - mMotionHelper.movePip(mTmpBounds); - result = true; - break; - case AccessibilityNodeInfo.ACTION_EXPAND: - mMotionHelper.expandLeavePip(); - result = true; - break; - default: - // Leave result as false - } - } - } - try { - callback.setPerformAccessibilityActionResult(result, interactionId); - } catch (RemoteException re) { - /* best effort - ignore */ - } - } - - private void setToExpandedBounds() { - float savedSnapFraction = mSnapAlgorithm.getSnapFraction( - new Rect(mTaskOrganizer.getLastReportedBounds()), mNormalMovementBounds); - mSnapAlgorithm.applySnapFraction(mExpandedBounds, mExpandedMovementBounds, - savedSnapFraction); - mTaskOrganizer.scheduleFinishResizePip(mExpandedBounds, (Rect bounds) -> { - mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundCallback.run(); - }); - } - - private void setToNormalBounds() { - float savedSnapFraction = mSnapAlgorithm.getSnapFraction( - new Rect(mTaskOrganizer.getLastReportedBounds()), mExpandedMovementBounds); - mSnapAlgorithm.applySnapFraction(mNormalBounds, mNormalMovementBounds, savedSnapFraction); - mTaskOrganizer.scheduleFinishResizePip(mNormalBounds, (Rect bounds) -> { - mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundCallback.run(); - }); - } - - @Override - public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, - String viewId, Region interactiveRegion, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { - // We have no view with a proper ID - try { - callback.setFindAccessibilityNodeInfoResult(null, interactionId); - } catch (RemoteException re) { - /* best effort - ignore */ - } - } - - @Override - public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, - Region interactiveRegion, int interactionId, - IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { - // We have no view with text - try { - callback.setFindAccessibilityNodeInfoResult(null, interactionId); - } catch (RemoteException re) { - /* best effort - ignore */ - } - } - - @Override - public void findFocus(long accessibilityNodeId, int focusType, Region interactiveRegion, - int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { - // We have no view that can take focus - try { - callback.setFindAccessibilityNodeInfoResult(null, interactionId); - } catch (RemoteException re) { - /* best effort - ignore */ - } - } - - @Override - public void focusSearch(long accessibilityNodeId, int direction, Region interactiveRegion, - int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, - int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { - // We have no view that can take focus - try { - callback.setFindAccessibilityNodeInfoResult(null, interactionId); - } catch (RemoteException re) { - /* best effort - ignore */ - } - } - - @Override - public void clearAccessibilityFocus() { - // We should not be here. - } - - @Override - public void notifyOutsideTouch() { - // Do nothing. - } - - /** - * Update the normal and expanded bounds so they can be used for Resize. - */ - void onMovementBoundsChanged(Rect normalBounds, Rect expandedBounds, Rect normalMovementBounds, - Rect expandedMovementBounds) { - mNormalBounds.set(normalBounds); - mExpandedBounds.set(expandedBounds); - mNormalMovementBounds.set(normalMovementBounds); - mExpandedMovementBounds.set(expandedMovementBounds); - } - - /** - * Update the Root node with PIP Accessibility action items. - */ - public static AccessibilityNodeInfo obtainRootAccessibilityNodeInfo(Context context) { - AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); - info.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, - AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_MOVE_WINDOW); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); - info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_resize, - context.getString(R.string.accessibility_action_pip_resize))); - info.setImportantForAccessibility(true); - info.setClickable(true); - info.setVisibleToUser(true); - return info; - } - - private List<AccessibilityNodeInfo> getNodeList() { - if (mAccessibilityNodeInfoList == null) { - mAccessibilityNodeInfoList = new ArrayList<>(1); - } - AccessibilityNodeInfo info = obtainRootAccessibilityNodeInfo(mContext); - mAccessibilityNodeInfoList.clear(); - mAccessibilityNodeInfoList.add(info); - return mAccessibilityNodeInfoList; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipAppOpsListener.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipAppOpsListener.java deleted file mode 100644 index 7dfd99c2110d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipAppOpsListener.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2017 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.pip.phone; - -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE; - -import android.app.AppOpsManager; -import android.app.AppOpsManager.OnOpChangedListener; -import android.app.IActivityManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Handler; -import android.util.Pair; - -public class PipAppOpsListener { - private static final String TAG = PipAppOpsListener.class.getSimpleName(); - - private Context mContext; - private Handler mHandler; - private IActivityManager mActivityManager; - private AppOpsManager mAppOpsManager; - private Callback mCallback; - - private AppOpsManager.OnOpChangedListener mAppOpsChangedListener = new OnOpChangedListener() { - @Override - public void onOpChanged(String op, String packageName) { - try { - // Dismiss the PiP once the user disables the app ops setting for that package - final Pair<ComponentName, Integer> topPipActivityInfo = - PipUtils.getTopPipActivity(mContext, mActivityManager); - if (topPipActivityInfo.first != null) { - final ApplicationInfo appInfo = mContext.getPackageManager() - .getApplicationInfoAsUser(packageName, 0, topPipActivityInfo.second); - if (appInfo.packageName.equals(topPipActivityInfo.first.getPackageName()) && - mAppOpsManager.checkOpNoThrow(OP_PICTURE_IN_PICTURE, appInfo.uid, - packageName) != MODE_ALLOWED) { - mHandler.post(() -> mCallback.dismissPip()); - } - } - } catch (NameNotFoundException e) { - // Unregister the listener if the package can't be found - unregisterAppOpsListener(); - } - } - }; - - public PipAppOpsListener(Context context, IActivityManager activityManager, - Callback callback) { - mContext = context; - mHandler = new Handler(mContext.getMainLooper()); - mActivityManager = activityManager; - mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - mCallback = callback; - } - - public void onActivityPinned(String packageName) { - // Register for changes to the app ops setting for this package while it is in PiP - registerAppOpsListener(packageName); - } - - public void onActivityUnpinned() { - // Unregister for changes to the previously PiP'ed package - unregisterAppOpsListener(); - } - - private void registerAppOpsListener(String packageName) { - mAppOpsManager.startWatchingMode(OP_PICTURE_IN_PICTURE, packageName, - mAppOpsChangedListener); - } - - private void unregisterAppOpsListener() { - mAppOpsManager.stopWatchingMode(mAppOpsChangedListener); - } - - /** Callback for PipAppOpsListener to request changes to the PIP window. */ - public interface Callback { - /** Dismisses the PIP window. */ - void dismissPip(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipController.java deleted file mode 100644 index 5bb179484e5d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipController.java +++ /dev/null @@ -1,490 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.phone; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; - -import static com.android.systemui.pip.PipAnimationController.isOutPipDirection; - -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.ActivityTaskManager; -import android.app.ActivityTaskManager.RootTaskInfo; -import android.app.IActivityManager; -import android.app.RemoteAction; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.content.res.Configuration; -import android.graphics.Rect; -import android.os.Handler; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; -import android.util.Pair; -import android.view.DisplayInfo; -import android.view.IPinnedStackController; -import android.window.WindowContainerTransaction; - -import com.android.systemui.Dependency; -import com.android.systemui.UiOffloadThread; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.model.SysUiState; -import com.android.systemui.pip.Pip; -import com.android.systemui.pip.PipBoundsHandler; -import com.android.systemui.pip.PipSurfaceTransactionHelper; -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.systemui.pip.PipUiEventLogger; -import com.android.systemui.shared.recents.IPinnedStackAnimationListener; -import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.InputConsumerController; -import com.android.systemui.shared.system.PinnedStackListenerForwarder.PinnedStackListener; -import com.android.systemui.shared.system.TaskStackChangeListener; -import com.android.systemui.shared.system.WindowManagerWrapper; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.util.DeviceConfigProxy; -import com.android.systemui.util.FloatingContentCoordinator; -import com.android.wm.shell.common.DisplayChangeController; -import com.android.wm.shell.common.DisplayController; - -import java.io.PrintWriter; - -/** - * Manages the picture-in-picture (PIP) UI and states for Phones. - */ -@SysUISingleton -public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback { - private static final String TAG = "PipController"; - - private Context mContext; - private IActivityManager mActivityManager; - private Handler mHandler = new Handler(); - - private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); - private final Rect mTmpInsetBounds = new Rect(); - private final Rect mTmpNormalBounds = new Rect(); - protected final Rect mReentryBounds = new Rect(); - - private DisplayController mDisplayController; - private InputConsumerController mInputConsumerController; - private PipAppOpsListener mAppOpsListener; - private PipBoundsHandler mPipBoundsHandler; - private PipMediaController mMediaController; - private PipTouchHandler mTouchHandler; - private PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; - private IPinnedStackAnimationListener mPinnedStackAnimationRecentsListener; - private boolean mIsInFixedRotation; - - protected PipMenuActivityController mMenuController; - protected PipTaskOrganizer mPipTaskOrganizer; - - /** - * Handler for display rotation changes. - */ - private final DisplayChangeController.OnDisplayChangingListener mRotationController = ( - int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> { - if (!mPipTaskOrganizer.isInPip() || mPipTaskOrganizer.isDeferringEnterPipAnimation()) { - // Skip if we aren't in PIP or haven't actually entered PIP yet. We still need to update - // the display layout in the bounds handler in this case. - mPipBoundsHandler.onDisplayRotationChangedNotInPip(mContext, toRotation); - return; - } - // If there is an animation running (ie. from a shelf offset), then ensure that we calculate - // the bounds for the next orientation using the destination bounds of the animation - // TODO: Techincally this should account for movement animation bounds as well - Rect currentBounds = mPipTaskOrganizer.getCurrentOrAnimatingBounds(); - final boolean changed = mPipBoundsHandler.onDisplayRotationChanged(mContext, - mTmpNormalBounds, currentBounds, mTmpInsetBounds, displayId, fromRotation, - toRotation, t); - if (changed) { - // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the - // movement bounds - mTouchHandler.adjustBoundsForRotation(mTmpNormalBounds, - mPipTaskOrganizer.getLastReportedBounds(), mTmpInsetBounds); - - // The bounds are being applied to a specific snap fraction, so reset any known offsets - // for the previous orientation before updating the movement bounds. - // We perform the resets if and only if this callback is due to screen rotation but - // not during the fixed rotation. In fixed rotation case, app is about to enter PiP - // and we need the offsets preserved to calculate the destination bounds. - if (!mIsInFixedRotation) { - mPipBoundsHandler.setShelfHeight(false, 0); - mPipBoundsHandler.onImeVisibilityChanged(false, 0); - mTouchHandler.onShelfVisibilityChanged(false, 0); - mTouchHandler.onImeVisibilityChanged(false, 0); - } - - updateMovementBounds(mTmpNormalBounds, true /* fromRotation */, - false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t); - } - }; - - private DisplayController.OnDisplaysChangedListener mFixedRotationListener = - new DisplayController.OnDisplaysChangedListener() { - @Override - public void onFixedRotationStarted(int displayId, int newRotation) { - mIsInFixedRotation = true; - } - - @Override - public void onFixedRotationFinished(int displayId) { - mIsInFixedRotation = false; - } - - @Override - public void onDisplayAdded(int displayId) { - mPipBoundsHandler.setDisplayLayout( - mDisplayController.getDisplayLayout(displayId)); - } - }; - - /** - * Handler for system task stack changes. - */ - private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { - @Override - public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { - mTouchHandler.onActivityPinned(); - mMediaController.onActivityPinned(); - mMenuController.onActivityPinned(); - mAppOpsListener.onActivityPinned(packageName); - - Dependency.get(UiOffloadThread.class).execute(() -> { - WindowManagerWrapper.getInstance().setPipVisibility(true); - }); - } - - @Override - public void onActivityUnpinned() { - final Pair<ComponentName, Integer> topPipActivityInfo = PipUtils.getTopPipActivity( - mContext, mActivityManager); - final ComponentName topActivity = topPipActivityInfo.first; - mMenuController.onActivityUnpinned(); - mTouchHandler.onActivityUnpinned(topActivity); - mAppOpsListener.onActivityUnpinned(); - - Dependency.get(UiOffloadThread.class).execute(() -> { - WindowManagerWrapper.getInstance().setPipVisibility(topActivity != null); - }); - } - - @Override - public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, - boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { - if (task.configuration.windowConfiguration.getWindowingMode() - != WINDOWING_MODE_PINNED) { - return; - } - mTouchHandler.getMotionHelper().expandLeavePip(clearedTask /* skipAnimation */); - } - }; - - /** - * Handler for messages from the PIP controller. - */ - private class PipControllerPinnedStackListener extends PinnedStackListener { - @Override - public void onListenerRegistered(IPinnedStackController controller) { - mHandler.post(() -> mTouchHandler.setPinnedStackController(controller)); - } - - @Override - public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { - mHandler.post(() -> { - mPipBoundsHandler.onImeVisibilityChanged(imeVisible, imeHeight); - mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); - }); - } - - @Override - public void onMovementBoundsChanged(boolean fromImeAdjustment) { - mHandler.post(() -> updateMovementBounds(null /* toBounds */, - false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */, - null /* windowContainerTransaction */)); - } - - @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - mHandler.post(() -> mMenuController.setAppActions(actions)); - } - - @Override - public void onActivityHidden(ComponentName componentName) { - mHandler.post(() -> mPipBoundsHandler.onResetReentryBounds(componentName)); - } - - @Override - public void onDisplayInfoChanged(DisplayInfo displayInfo) { - mHandler.post(() -> mPipBoundsHandler.onDisplayInfoChanged(displayInfo)); - } - - @Override - public void onConfigurationChanged() { - mHandler.post(() -> mPipBoundsHandler.onConfigurationChanged(mContext)); - } - - @Override - public void onAspectRatioChanged(float aspectRatio) { - mHandler.post(() -> { - mPipBoundsHandler.onAspectRatioChanged(aspectRatio); - mTouchHandler.onAspectRatioChanged(); - }); - } - } - - public ConfigurationController.ConfigurationListener mOverlayChangedListener = - new ConfigurationController.ConfigurationListener() { - @Override - public void onOverlayChanged() { - mHandler.post(() -> { - mPipBoundsHandler.onOverlayChanged(mContext, mContext.getDisplay()); - updateMovementBounds(null /* toBounds */, - false /* fromRotation */, false /* fromImeAdjustment */, - false /* fromShelfAdjustment */, - null /* windowContainerTransaction */); - }); - } - }; - - public PipController(Context context, BroadcastDispatcher broadcastDispatcher, - ConfigurationController configController, - DeviceConfigProxy deviceConfig, - DisplayController displayController, - FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState, - PipBoundsHandler pipBoundsHandler, - PipSurfaceTransactionHelper pipSurfaceTransactionHelper, - PipTaskOrganizer pipTaskOrganizer, - PipUiEventLogger pipUiEventLogger) { - mContext = context; - mActivityManager = ActivityManager.getService(); - - PackageManager pm = context.getPackageManager(); - boolean supportsPip = pm.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE); - if (supportsPip) { - initController(context, broadcastDispatcher, configController, deviceConfig, - displayController, floatingContentCoordinator, sysUiState, pipBoundsHandler, - pipSurfaceTransactionHelper, pipTaskOrganizer, pipUiEventLogger); - } else { - Log.w(TAG, "Device not support PIP feature"); - } - } - - private void initController(Context context, BroadcastDispatcher broadcastDispatcher, - ConfigurationController configController, - DeviceConfigProxy deviceConfig, - DisplayController displayController, - FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState, - PipBoundsHandler pipBoundsHandler, - PipSurfaceTransactionHelper pipSurfaceTransactionHelper, - PipTaskOrganizer pipTaskOrganizer, - PipUiEventLogger pipUiEventLogger) { - - // Ensure that we are the primary user's SystemUI. - final int processUser = UserManager.get(context).getUserHandle(); - if (processUser != UserHandle.USER_SYSTEM) { - throw new IllegalStateException("Non-primary Pip component not currently supported."); - } - - try { - WindowManagerWrapper.getInstance().addPinnedStackListener( - new PipControllerPinnedStackListener()); - } catch (RemoteException e) { - Log.e(TAG, "Failed to register pinned stack listener", e); - } - ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); - - mDisplayController = displayController; - mPipBoundsHandler = pipBoundsHandler; - mPipSurfaceTransactionHelper = pipSurfaceTransactionHelper; - mPipTaskOrganizer = pipTaskOrganizer; - mPipTaskOrganizer.registerPipTransitionCallback(this); - mInputConsumerController = InputConsumerController.getPipInputConsumer(); - mMediaController = new PipMediaController(context, mActivityManager, broadcastDispatcher); - mMenuController = new PipMenuActivityController(context, - mMediaController, mInputConsumerController, mPipTaskOrganizer); - mTouchHandler = new PipTouchHandler(context, mActivityManager, - mMenuController, mInputConsumerController, mPipBoundsHandler, mPipTaskOrganizer, - floatingContentCoordinator, deviceConfig, sysUiState, pipUiEventLogger); - mAppOpsListener = new PipAppOpsListener(context, mActivityManager, - mTouchHandler.getMotionHelper()); - displayController.addDisplayChangingController(mRotationController); - displayController.addDisplayWindowListener(mFixedRotationListener); - - // Ensure that we have the display info in case we get calls to update the bounds before the - // listener calls back - final DisplayInfo displayInfo = new DisplayInfo(); - context.getDisplay().getDisplayInfo(displayInfo); - mPipBoundsHandler.onDisplayInfoChanged(displayInfo); - - configController.addCallback(mOverlayChangedListener); - - try { - RootTaskInfo taskInfo = ActivityTaskManager.getService().getRootTaskInfo( - WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); - if (taskInfo != null) { - // If SystemUI restart, and it already existed a pinned stack, - // register the pip input consumer to ensure touch can send to it. - mInputConsumerController.registerInputConsumer(true /* withSfVsync */); - } - } catch (RemoteException | UnsupportedOperationException e) { - e.printStackTrace(); - } - } - - /** - * Updates the PIP per configuration changed. - */ - public void onConfigurationChanged(Configuration newConfig) { - mTouchHandler.onConfigurationChanged(); - } - - /** - * Expands the PIP. - */ - @Override - public void expandPip() { - mTouchHandler.getMotionHelper().expandLeavePip(false /* skipAnimation */); - } - - /** - * Hides the PIP menu. - */ - @Override - public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) { - mMenuController.hideMenu(onStartCallback, onEndCallback); - } - - /** - * Sent from KEYCODE_WINDOW handler in PhoneWindowManager, to request the menu to be shown. - */ - public void showPictureInPictureMenu() { - mTouchHandler.showPictureInPictureMenu(); - } - - /** - * Sets a customized touch gesture that replaces the default one. - */ - public void setTouchGesture(PipTouchGesture gesture) { - mTouchHandler.setTouchGesture(gesture); - } - - /** - * Sets both shelf visibility and its height. - */ - @Override - public void setShelfHeight(boolean visible, int height) { - mHandler.post(() -> { - final int shelfHeight = visible ? height : 0; - final boolean changed = mPipBoundsHandler.setShelfHeight(visible, shelfHeight); - if (changed) { - mTouchHandler.onShelfVisibilityChanged(visible, shelfHeight); - updateMovementBounds(mPipTaskOrganizer.getLastReportedBounds(), - false /* fromRotation */, false /* fromImeAdjustment */, - true /* fromShelfAdjustment */, null /* windowContainerTransaction */); - } - }); - } - - @Override - public void setPinnedStackAnimationType(int animationType) { - mHandler.post(() -> mPipTaskOrganizer.setOneShotAnimationType(animationType)); - } - - @Override - public void setPinnedStackAnimationListener(IPinnedStackAnimationListener listener) { - mHandler.post(() -> mPinnedStackAnimationRecentsListener = listener); - } - - @Override - public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { - if (isOutPipDirection(direction)) { - // Exiting PIP, save the reentry bounds to restore to when re-entering. - updateReentryBounds(pipBounds); - mPipBoundsHandler.onSaveReentryBounds(activity, mReentryBounds); - } - // Disable touches while the animation is running - mTouchHandler.setTouchEnabled(false); - if (mPinnedStackAnimationRecentsListener != null) { - try { - mPinnedStackAnimationRecentsListener.onPinnedStackAnimationStarted(); - } catch (RemoteException e) { - Log.e(TAG, "Failed to callback recents", e); - } - } - } - - /** - * Update the bounds used to save the re-entry size and snap fraction when exiting PIP. - */ - public void updateReentryBounds(Rect bounds) { - final Rect reentryBounds = mTouchHandler.getUserResizeBounds(); - float snapFraction = mPipBoundsHandler.getSnapFraction(bounds); - mPipBoundsHandler.applySnapFraction(reentryBounds, snapFraction); - mReentryBounds.set(reentryBounds); - } - - @Override - public void onPipTransitionFinished(ComponentName activity, int direction) { - onPipTransitionFinishedOrCanceled(direction); - } - - @Override - public void onPipTransitionCanceled(ComponentName activity, int direction) { - onPipTransitionFinishedOrCanceled(direction); - } - - private void onPipTransitionFinishedOrCanceled(int direction) { - // Re-enable touches after the animation completes - mTouchHandler.setTouchEnabled(true); - mTouchHandler.onPinnedStackAnimationEnded(direction); - mMenuController.onPinnedStackAnimationEnded(); - } - - private void updateMovementBounds(@Nullable Rect toBounds, boolean fromRotation, - boolean fromImeAdjustment, boolean fromShelfAdjustment, - WindowContainerTransaction wct) { - // Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before - // passing to mTouchHandler/mPipTaskOrganizer - final Rect outBounds = new Rect(toBounds); - mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds, - outBounds, mTmpDisplayInfo); - // mTouchHandler would rely on the bounds populated from mPipTaskOrganizer - mPipTaskOrganizer.onMovementBoundsChanged(outBounds, fromRotation, fromImeAdjustment, - fromShelfAdjustment, wct); - mTouchHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds, - outBounds, fromImeAdjustment, fromShelfAdjustment, - mTmpDisplayInfo.rotation); - } - - @Override - public void dump(PrintWriter pw) { - final String innerPrefix = " "; - pw.println(TAG); - mInputConsumerController.dump(pw, innerPrefix); - mMenuController.dump(pw, innerPrefix); - mTouchHandler.dump(pw, innerPrefix); - mPipBoundsHandler.dump(pw, innerPrefix); - mPipTaskOrganizer.dump(pw, innerPrefix); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMediaController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMediaController.java deleted file mode 100644 index 361aafacdf76..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMediaController.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (C) 2017 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.pip.phone; - -import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; - -import android.app.IActivityManager; -import android.app.PendingIntent; -import android.app.RemoteAction; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.media.session.MediaSessionManager; -import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener; -import android.media.session.PlaybackState; -import android.os.UserHandle; - -import com.android.systemui.Dependency; -import com.android.systemui.R; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.statusbar.policy.UserInfoController; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only - * if there are no actions from the PiP activity itself). The active media controller is only set - * when there is a media session from the top PiP activity. - */ -public class PipMediaController { - - private static final String ACTION_PLAY = "com.android.systemui.pip.phone.PLAY"; - private static final String ACTION_PAUSE = "com.android.systemui.pip.phone.PAUSE"; - private static final String ACTION_NEXT = "com.android.systemui.pip.phone.NEXT"; - private static final String ACTION_PREV = "com.android.systemui.pip.phone.PREV"; - - /** - * A listener interface to receive notification on changes to the media actions. - */ - public interface ActionListener { - /** - * Called when the media actions changes. - */ - void onMediaActionsChanged(List<RemoteAction> actions); - } - - private final Context mContext; - private final IActivityManager mActivityManager; - - private final MediaSessionManager mMediaSessionManager; - private MediaController mMediaController; - - private RemoteAction mPauseAction; - private RemoteAction mPlayAction; - private RemoteAction mNextAction; - private RemoteAction mPrevAction; - - private BroadcastReceiver mPlayPauseActionReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (action.equals(ACTION_PLAY)) { - mMediaController.getTransportControls().play(); - } else if (action.equals(ACTION_PAUSE)) { - mMediaController.getTransportControls().pause(); - } else if (action.equals(ACTION_NEXT)) { - mMediaController.getTransportControls().skipToNext(); - } else if (action.equals(ACTION_PREV)) { - mMediaController.getTransportControls().skipToPrevious(); - } - } - }; - - private final MediaController.Callback mPlaybackChangedListener = new MediaController.Callback() { - @Override - public void onPlaybackStateChanged(PlaybackState state) { - notifyActionsChanged(); - } - }; - - private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener = - new OnActiveSessionsChangedListener() { - @Override - public void onActiveSessionsChanged(List<MediaController> controllers) { - resolveActiveMediaController(controllers); - } - }; - - private ArrayList<ActionListener> mListeners = new ArrayList<>(); - - public PipMediaController(Context context, IActivityManager activityManager, - BroadcastDispatcher broadcastDispatcher) { - mContext = context; - mActivityManager = activityManager; - IntentFilter mediaControlFilter = new IntentFilter(); - mediaControlFilter.addAction(ACTION_PLAY); - mediaControlFilter.addAction(ACTION_PAUSE); - mediaControlFilter.addAction(ACTION_NEXT); - mediaControlFilter.addAction(ACTION_PREV); - broadcastDispatcher.registerReceiver(mPlayPauseActionReceiver, mediaControlFilter); - - createMediaActions(); - mMediaSessionManager = - (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); - - // The media session listener needs to be re-registered when switching users - UserInfoController userInfoController = Dependency.get(UserInfoController.class); - userInfoController.addCallback((String name, Drawable picture, String userAccount) -> - registerSessionListenerForCurrentUser()); - } - - /** - * Handles when an activity is pinned. - */ - public void onActivityPinned() { - // Once we enter PiP, try to find the active media controller for the top most activity - resolveActiveMediaController(mMediaSessionManager.getActiveSessionsForUser(null, - UserHandle.USER_CURRENT)); - } - - /** - * Adds a new media action listener. - */ - public void addListener(ActionListener listener) { - if (!mListeners.contains(listener)) { - mListeners.add(listener); - listener.onMediaActionsChanged(getMediaActions()); - } - } - - /** - * Removes a media action listener. - */ - public void removeListener(ActionListener listener) { - listener.onMediaActionsChanged(Collections.EMPTY_LIST); - mListeners.remove(listener); - } - - /** - * Gets the set of media actions currently available. - */ - private List<RemoteAction> getMediaActions() { - if (mMediaController == null || mMediaController.getPlaybackState() == null) { - return Collections.EMPTY_LIST; - } - - ArrayList<RemoteAction> mediaActions = new ArrayList<>(); - int state = mMediaController.getPlaybackState().getState(); - boolean isPlaying = MediaSession.isActiveState(state); - long actions = mMediaController.getPlaybackState().getActions(); - - // Prev action - mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0); - mediaActions.add(mPrevAction); - - // Play/pause action - if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) { - mediaActions.add(mPlayAction); - } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) { - mediaActions.add(mPauseAction); - } - - // Next action - mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0); - mediaActions.add(mNextAction); - return mediaActions; - } - - /** - * Creates the standard media buttons that we may show. - */ - private void createMediaActions() { - String pauseDescription = mContext.getString(R.string.pip_pause); - mPauseAction = new RemoteAction(Icon.createWithResource(mContext, - R.drawable.pip_ic_pause_white), pauseDescription, pauseDescription, - PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PAUSE), - FLAG_UPDATE_CURRENT)); - - String playDescription = mContext.getString(R.string.pip_play); - mPlayAction = new RemoteAction(Icon.createWithResource(mContext, - R.drawable.pip_ic_play_arrow_white), playDescription, playDescription, - PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PLAY), - FLAG_UPDATE_CURRENT)); - - String nextDescription = mContext.getString(R.string.pip_skip_to_next); - mNextAction = new RemoteAction(Icon.createWithResource(mContext, - R.drawable.ic_skip_next_white), nextDescription, nextDescription, - PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NEXT), - FLAG_UPDATE_CURRENT)); - - String prevDescription = mContext.getString(R.string.pip_skip_to_prev); - mPrevAction = new RemoteAction(Icon.createWithResource(mContext, - R.drawable.ic_skip_previous_white), prevDescription, prevDescription, - PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PREV), - FLAG_UPDATE_CURRENT)); - } - - /** - * Re-registers the session listener for the current user. - */ - private void registerSessionListenerForCurrentUser() { - mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener); - mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsChangedListener, null, - UserHandle.USER_CURRENT, null); - } - - /** - * Tries to find and set the active media controller for the top PiP activity. - */ - private void resolveActiveMediaController(List<MediaController> controllers) { - if (controllers != null) { - final ComponentName topActivity = PipUtils.getTopPipActivity(mContext, - mActivityManager).first; - if (topActivity != null) { - for (int i = 0; i < controllers.size(); i++) { - final MediaController controller = controllers.get(i); - if (controller.getPackageName().equals(topActivity.getPackageName())) { - setActiveMediaController(controller); - return; - } - } - } - } - setActiveMediaController(null); - } - - /** - * Sets the active media controller for the top PiP activity. - */ - private void setActiveMediaController(MediaController controller) { - if (controller != mMediaController) { - if (mMediaController != null) { - mMediaController.unregisterCallback(mPlaybackChangedListener); - } - mMediaController = controller; - if (controller != null) { - controller.registerCallback(mPlaybackChangedListener); - } - notifyActionsChanged(); - - // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV) - } - } - - /** - * Notifies all listeners that the actions have changed. - */ - private void notifyActionsChanged() { - if (!mListeners.isEmpty()) { - List<RemoteAction> actions = getMediaActions(); - mListeners.forEach(l -> l.onMediaActionsChanged(actions)); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java deleted file mode 100644 index 5b07db6c91a1..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivityController.java +++ /dev/null @@ -1,397 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.phone; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; - -import android.app.ActivityTaskManager; -import android.app.ActivityTaskManager.RootTaskInfo; -import android.app.RemoteAction; -import android.content.Context; -import android.content.pm.ParceledListSlice; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.os.Debug; -import android.os.RemoteException; -import android.util.Log; -import android.view.MotionEvent; -import android.view.WindowManager; - -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.systemui.pip.phone.PipMediaController.ActionListener; -import com.android.systemui.shared.system.InputConsumerController; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -/** - * Manages the PiP menu activity which can show menu options or a scrim. - * - * The current media session provides actions whenever there are no valid actions provided by the - * current PiP activity. Otherwise, those actions always take precedence. - */ -public class PipMenuActivityController { - - private static final String TAG = "PipMenuActController"; - private static final boolean DEBUG = false; - - public static final int MENU_STATE_NONE = 0; - public static final int MENU_STATE_CLOSE = 1; - public static final int MENU_STATE_FULL = 2; - - /** - * A listener interface to receive notification on changes in PIP. - */ - public interface Listener { - /** - * Called when the PIP menu visibility changes. - * - * @param menuState the current state of the menu - * @param resize whether or not to resize the PiP with the state change - */ - void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback); - - /** - * Called when the PIP requested to be expanded. - */ - void onPipExpand(); - - /** - * Called when the PIP requested to be dismissed. - */ - void onPipDismiss(); - - /** - * Called when the PIP requested to show the menu. - */ - void onPipShowMenu(); - } - - private Context mContext; - private PipTaskOrganizer mPipTaskOrganizer; - private PipMediaController mMediaController; - private InputConsumerController mInputConsumerController; - - private ArrayList<Listener> mListeners = new ArrayList<>(); - private ParceledListSlice<RemoteAction> mAppActions; - private ParceledListSlice<RemoteAction> mMediaActions; - private int mMenuState; - - private PipMenuView mPipMenuView; - - private ActionListener mMediaActionListener = new ActionListener() { - @Override - public void onMediaActionsChanged(List<RemoteAction> mediaActions) { - mMediaActions = new ParceledListSlice<>(mediaActions); - updateMenuActions(); - } - }; - - public PipMenuActivityController(Context context, - PipMediaController mediaController, InputConsumerController inputConsumerController, - PipTaskOrganizer pipTaskOrganizer - ) { - mContext = context; - mMediaController = mediaController; - mInputConsumerController = inputConsumerController; - mPipTaskOrganizer = pipTaskOrganizer; - } - - public boolean isMenuVisible() { - return mPipMenuView != null && mMenuState != MENU_STATE_NONE; - } - - public void onActivityPinned() { - attachPipMenuView(); - mInputConsumerController.registerInputConsumer(true /* withSfVsync */); - } - - public void onActivityUnpinned() { - hideMenu(); - mInputConsumerController.unregisterInputConsumer(); - mPipTaskOrganizer.detachPipMenuViewHost(); - mPipMenuView = null; - } - - public void onPinnedStackAnimationEnded() { - if (isMenuVisible()) { - mPipMenuView.onPipAnimationEnded(); - } - } - - private void attachPipMenuView() { - if (mPipMenuView == null) { - mPipMenuView = new PipMenuView(mContext, this); - - } - mPipTaskOrganizer.attachPipMenuViewHost(mPipMenuView, getPipMenuLayoutParams(0, 0)); - } - - /** - * Adds a new menu activity listener. - */ - public void addListener(Listener listener) { - if (!mListeners.contains(listener)) { - mListeners.add(listener); - } - } - - /** - * Updates the appearance of the menu and scrim on top of the PiP while dismissing. - */ - public void setDismissFraction(float fraction) { - final boolean isMenuVisible = isMenuVisible(); - if (DEBUG) { - Log.d(TAG, "setDismissFraction() isMenuVisible=" + isMenuVisible - + " fraction=" + fraction); - } - if (isMenuVisible) { - mPipMenuView.updateDismissFraction(fraction); - } - } - - /** - * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu - * upon PiP window transition is finished. - */ - public void showMenuWithDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, - boolean willResizeMenu, boolean showResizeHandle) { - // hide all visible controls including close button and etc. first, this is to ensure - // menu is totally invisible during the transition to eliminate unpleasant artifacts - fadeOutMenu(); - showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, - true /* withDelay */, showResizeHandle); - } - - /** - * Shows the menu activity immediately. - */ - public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, - boolean willResizeMenu, boolean showResizeHandle) { - showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, - false /* withDelay */, showResizeHandle); - } - - private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, - boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) { - if (DEBUG) { - Log.d(TAG, "showMenu() state=" + menuState - + " isMenuVisible=" + isMenuVisible() - + " allowMenuTimeout=" + allowMenuTimeout - + " willResizeMenu=" + willResizeMenu - + " withDelay=" + withDelay - + " showResizeHandle=" + showResizeHandle - + " callers=\n" + Debug.getCallers(5, " ")); - } - - if (!mPipTaskOrganizer.isPipMenuViewHostAttached()) { - Log.d(TAG, "PipMenu has not been attached yet. Attaching now at showMenuInternal()."); - attachPipMenuView(); - } - - mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay, - showResizeHandle); - } - - /** - * Pokes the menu, indicating that the user is interacting with it. - */ - public void pokeMenu() { - final boolean isMenuVisible = isMenuVisible(); - if (DEBUG) { - Log.d(TAG, "pokeMenu() isMenuVisible=" + isMenuVisible); - } - if (isMenuVisible) { - mPipMenuView.pokeMenu(); - } - } - - private void fadeOutMenu() { - final boolean isMenuVisible = isMenuVisible(); - if (DEBUG) { - Log.d(TAG, "fadeOutMenu() isMenuVisible=" + isMenuVisible); - } - if (isMenuVisible) { - mPipMenuView.fadeOutMenu(); - } - } - - /** - * Hides the menu activity. - */ - public void hideMenu() { - final boolean isMenuVisible = isMenuVisible(); - if (DEBUG) { - Log.d(TAG, "hideMenu() state=" + mMenuState - + " isMenuVisible=" + isMenuVisible - + " callers=\n" + Debug.getCallers(5, " ")); - } - if (isMenuVisible) { - mPipMenuView.hideMenu(); - } - } - - /** - * Hides the menu activity. - */ - public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) { - if (isMenuVisible()) { - // If the menu is visible in either the closed or full state, then hide the menu and - // trigger the animation trigger afterwards - onStartCallback.run(); - mPipMenuView.hideMenu(onEndCallback); - } - } - - /** - * Preemptively mark the menu as invisible, used when we are directly manipulating the pinned - * stack and don't want to trigger a resize which can animate the stack in a conflicting way - * (ie. when manually expanding or dismissing). - */ - public void hideMenuWithoutResize() { - onMenuStateChanged(MENU_STATE_NONE, false /* resize */, null /* callback */); - } - - /** - * Sets the menu actions to the actions provided by the current PiP activity. - */ - public void setAppActions(ParceledListSlice<RemoteAction> appActions) { - mAppActions = appActions; - updateMenuActions(); - } - - void onPipExpand() { - mListeners.forEach(Listener::onPipExpand); - } - - void onPipDismiss() { - mListeners.forEach(Listener::onPipDismiss); - } - - void onPipShowMenu() { - mListeners.forEach(Listener::onPipShowMenu); - } - - /** - * @return the best set of actions to show in the PiP menu. - */ - private ParceledListSlice<RemoteAction> resolveMenuActions() { - if (isValidActions(mAppActions)) { - return mAppActions; - } - return mMediaActions; - } - - /** - * Returns a default LayoutParams for the PIP Menu. - * @param width the PIP stack width. - * @param height the PIP stack height. - */ - public static WindowManager.LayoutParams getPipMenuLayoutParams(int width, int height) { - return new WindowManager.LayoutParams(width, height, - WindowManager.LayoutParams.TYPE_APPLICATION, 0, PixelFormat.TRANSLUCENT); - } - - /** - * Updates the PiP menu with the best set of actions provided. - */ - private void updateMenuActions() { - if (isMenuVisible()) { - // Fetch the pinned stack bounds - Rect stackBounds = null; - try { - RootTaskInfo pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo( - WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); - if (pinnedTaskInfo != null) { - stackBounds = pinnedTaskInfo.bounds; - } - } catch (RemoteException e) { - Log.e(TAG, "Error showing PIP menu", e); - } - - mPipMenuView.setActions(stackBounds, resolveMenuActions().getList()); - } - } - - /** - * Returns whether the set of actions are valid. - */ - private static boolean isValidActions(ParceledListSlice<?> actions) { - return actions != null && actions.getList().size() > 0; - } - - /** - * Handles changes in menu visibility. - */ - void onMenuStateChanged(int menuState, boolean resize, Runnable callback) { - if (DEBUG) { - Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState - + " menuState=" + menuState + " resize=" + resize - + " callers=\n" + Debug.getCallers(5, " ")); - } - - if (menuState != mMenuState) { - mListeners.forEach(l -> l.onPipMenuStateChanged(menuState, resize, callback)); - if (menuState == MENU_STATE_FULL) { - // Once visible, start listening for media action changes. This call will trigger - // the menu actions to be updated again. - mMediaController.addListener(mMediaActionListener); - } else { - // Once hidden, stop listening for media action changes. This call will trigger - // the menu actions to be updated again. - mMediaController.removeListener(mMediaActionListener); - } - } - mMenuState = menuState; - } - - /** - * Handles a pointer event sent from pip input consumer. - */ - void handlePointerEvent(MotionEvent ev) { - if (ev.isTouchEvent()) { - mPipMenuView.dispatchTouchEvent(ev); - } else { - mPipMenuView.dispatchGenericMotionEvent(ev); - } - } - - /** - * Tell the PIP Menu to recalculate its layout given its current position on the display. - */ - public void updateMenuLayout(Rect bounds) { - final boolean isMenuVisible = isMenuVisible(); - if (DEBUG) { - Log.d(TAG, "updateMenuLayout() state=" + mMenuState - + " isMenuVisible=" + isMenuVisible - + " callers=\n" + Debug.getCallers(5, " ")); - } - if (isMenuVisible) { - mPipMenuView.updateMenuLayout(bounds); - } - } - - public void dump(PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.println(prefix + TAG); - pw.println(innerPrefix + "mMenuState=" + mMenuState); - pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView); - pw.println(innerPrefix + "mListeners=" + mListeners.size()); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuIconsAlgorithm.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuIconsAlgorithm.java deleted file mode 100644 index 6cfed070198b..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuIconsAlgorithm.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2020 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.pip.phone; - -import android.content.Context; -import android.graphics.Rect; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -/** - * Helper class to calculate and place the menu icons on the PIP Menu. - */ -public class PipMenuIconsAlgorithm { - - private static final String TAG = "PipMenuIconsAlgorithm"; - - private boolean mFinishedLayout = false; - protected ViewGroup mViewRoot; - protected ViewGroup mTopEndContainer; - protected View mDragHandle; - protected View mSettingsButton; - protected View mDismissButton; - - protected PipMenuIconsAlgorithm(Context context) { - } - - /** - * Bind the necessary views. - */ - public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle, - View settingsButton, View dismissButton) { - mViewRoot = viewRoot; - mTopEndContainer = topEndContainer; - mDragHandle = dragHandle; - mSettingsButton = settingsButton; - mDismissButton = dismissButton; - } - - /** - * Updates the position of the drag handle based on where the PIP window is on the screen. - */ - public void onBoundsChanged(Rect bounds) { - if (mViewRoot == null || mTopEndContainer == null || mDragHandle == null - || mSettingsButton == null || mDismissButton == null) { - Log.e(TAG, "One if the required views is null."); - } - - //We only need to calculate the layout once since it does not change. - if (!mFinishedLayout) { - mTopEndContainer.removeView(mSettingsButton); - mViewRoot.addView(mSettingsButton); - - setLayoutGravity(mDragHandle, Gravity.START | Gravity.TOP); - setLayoutGravity(mSettingsButton, Gravity.START | Gravity.TOP); - mFinishedLayout = true; - } - } - - /** - * Set the gravity on the given view. - */ - protected static void setLayoutGravity(View v, int gravity) { - if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) { - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams(); - params.gravity = gravity; - v.setLayoutParams(params); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuView.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuView.java deleted file mode 100644 index c66f442c4c0d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuView.java +++ /dev/null @@ -1,495 +0,0 @@ -/* - * Copyright (C) 2020 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.pip.phone; - -import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; -import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS; -import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; -import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; -import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; - -import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; -import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL; -import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.app.ActivityManager; -import android.app.PendingIntent.CanceledException; -import android.app.RemoteAction; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.UserHandle; -import android.util.Log; -import android.util.Pair; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.LinearLayout; - -import com.android.systemui.Interpolators; -import com.android.systemui.R; - -import java.util.ArrayList; -import java.util.List; - -/** - * Translucent window that gets started on top of a task in PIP to allow the user to control it. - */ -public class PipMenuView extends FrameLayout { - - private static final String TAG = "PipMenuView"; - - private static final int MESSAGE_INVALID_TYPE = -1; - public static final int MESSAGE_MENU_EXPANDED = 8; - - private static final int INITIAL_DISMISS_DELAY = 3500; - private static final int POST_INTERACTION_DISMISS_DELAY = 2000; - private static final long MENU_FADE_DURATION = 125; - private static final long MENU_SLOW_FADE_DURATION = 175; - private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30; - - private static final float MENU_BACKGROUND_ALPHA = 0.3f; - private static final float DISMISS_BACKGROUND_ALPHA = 0.6f; - - private static final float DISABLED_ACTION_ALPHA = 0.54f; - - private static final boolean ENABLE_RESIZE_HANDLE = false; - - private int mMenuState; - private boolean mResize = true; - private boolean mAllowMenuTimeout = true; - private boolean mAllowTouches = true; - - private final List<RemoteAction> mActions = new ArrayList<>(); - - private AccessibilityManager mAccessibilityManager; - private Drawable mBackgroundDrawable; - private View mMenuContainer; - private LinearLayout mActionsGroup; - private int mBetweenActionPaddingLand; - - private AnimatorSet mMenuContainerAnimator; - private PipMenuActivityController mController; - - private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener = - new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - final float alpha = (float) animation.getAnimatedValue(); - mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255)); - } - }; - - private Handler mHandler = new Handler(); - - private final Runnable mHideMenuRunnable = this::hideMenu; - - protected View mViewRoot; - protected View mSettingsButton; - protected View mDismissButton; - protected View mResizeHandle; - protected View mTopEndContainer; - protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; - - public PipMenuView(Context context, PipMenuActivityController controller) { - super(context, null, 0); - mContext = context; - mController = controller; - - mAccessibilityManager = context.getSystemService(AccessibilityManager.class); - inflate(context, R.layout.pip_menu, this); - - mBackgroundDrawable = new ColorDrawable(Color.BLACK); - mBackgroundDrawable.setAlpha(0); - mViewRoot = findViewById(R.id.background); - mViewRoot.setBackground(mBackgroundDrawable); - mMenuContainer = findViewById(R.id.menu_container); - mMenuContainer.setAlpha(0); - mTopEndContainer = findViewById(R.id.top_end_container); - mSettingsButton = findViewById(R.id.settings); - mSettingsButton.setAlpha(0); - mSettingsButton.setOnClickListener((v) -> { - if (v.getAlpha() != 0) { - showSettings(); - } - }); - mDismissButton = findViewById(R.id.dismiss); - mDismissButton.setAlpha(0); - mDismissButton.setOnClickListener(v -> dismissPip()); - findViewById(R.id.expand_button).setOnClickListener(v -> { - if (mMenuContainer.getAlpha() != 0) { - expandPip(); - } - }); - // TODO (b/161710689): Remove this once focusability for Windowless window is working - findViewById(R.id.expand_button).setFocusable(false); - mDismissButton.setFocusable(false); - mSettingsButton.setFocusable(false); - - mResizeHandle = findViewById(R.id.resize_handle); - mResizeHandle.setAlpha(0); - mActionsGroup = findViewById(R.id.actions_group); - mBetweenActionPaddingLand = getResources().getDimensionPixelSize( - R.dimen.pip_between_action_padding_land); - mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext); - mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer, - mResizeHandle, mSettingsButton, mDismissButton); - - initAccessibility(); - } - - private void initAccessibility() { - this.setAccessibilityDelegate(new View.AccessibilityDelegate() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - String label = getResources().getString(R.string.pip_menu_title); - info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label)); - } - - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) { - mController.onPipShowMenu(); - } - return super.performAccessibilityAction(host, action, args); - } - }); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_ESCAPE) { - hideMenu(); - return true; - } - return super.onKeyUp(keyCode, event); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - if (!mAllowTouches) { - return false; - } - - if (mAllowMenuTimeout) { - repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); - } - - return super.dispatchTouchEvent(ev); - } - - @Override - public boolean dispatchGenericMotionEvent(MotionEvent event) { - if (mAllowMenuTimeout) { - repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); - } - - return super.dispatchGenericMotionEvent(event); - } - - void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, - boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) { - mAllowMenuTimeout = allowMenuTimeout; - if (mMenuState != menuState) { - // Disallow touches if the menu needs to resize while showing, and we are transitioning - // to/from a full menu state. - boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow - && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL); - mAllowTouches = !disallowTouchesUntilAnimationEnd; - cancelDelayedHide(); - updateActionViews(stackBounds); - if (mMenuContainerAnimator != null) { - mMenuContainerAnimator.cancel(); - } - mMenuContainerAnimator = new AnimatorSet(); - ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, - mMenuContainer.getAlpha(), 1f); - menuAnim.addUpdateListener(mMenuBgUpdateListener); - ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, - mSettingsButton.getAlpha(), 1f); - ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, - mDismissButton.getAlpha(), 1f); - ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA, - mResizeHandle.getAlpha(), - ENABLE_RESIZE_HANDLE && menuState == MENU_STATE_CLOSE && showResizeHandle - ? 1f : 0f); - if (menuState == MENU_STATE_FULL) { - mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, - resizeAnim); - } else { - mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim); - } - mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN); - mMenuContainerAnimator.setDuration(menuState == MENU_STATE_CLOSE - ? MENU_FADE_DURATION - : MENU_SLOW_FADE_DURATION); - if (allowMenuTimeout) { - mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - repostDelayedHide(INITIAL_DISMISS_DELAY); - } - }); - } - if (withDelay) { - // starts the menu container animation after window expansion is completed - notifyMenuStateChange(menuState, resizeMenuOnShow, () -> { - if (mMenuContainerAnimator == null) { - return; - } - mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY); - mMenuContainerAnimator.start(); - }); - } else { - notifyMenuStateChange(menuState, resizeMenuOnShow, null); - mMenuContainerAnimator.start(); - } - } else { - // If we are already visible, then just start the delayed dismiss and unregister any - // existing input consumers from the previous drag - if (allowMenuTimeout) { - repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); - } - } - } - - /** - * Different from {@link #hideMenu()}, this function does not try to finish this menu activity - * and instead, it fades out the controls by setting the alpha to 0 directly without menu - * visibility callbacks invoked. - */ - void fadeOutMenu() { - mMenuContainer.setAlpha(0f); - mSettingsButton.setAlpha(0f); - mDismissButton.setAlpha(0f); - mResizeHandle.setAlpha(0f); - } - - void pokeMenu() { - cancelDelayedHide(); - } - - void onPipAnimationEnded() { - mAllowTouches = true; - } - - void updateMenuLayout(Rect bounds) { - mPipMenuIconsAlgorithm.onBoundsChanged(bounds); - } - - void hideMenu() { - hideMenu(null); - } - - void hideMenu(Runnable animationEndCallback) { - hideMenu(animationEndCallback, true /* notifyMenuVisibility */, false); - } - - private void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, - boolean animate) { - if (mMenuState != MENU_STATE_NONE) { - cancelDelayedHide(); - if (notifyMenuVisibility) { - notifyMenuStateChange(MENU_STATE_NONE, mResize, null); - } - mMenuContainerAnimator = new AnimatorSet(); - ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, - mMenuContainer.getAlpha(), 0f); - menuAnim.addUpdateListener(mMenuBgUpdateListener); - ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, - mSettingsButton.getAlpha(), 0f); - ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, - mDismissButton.getAlpha(), 0f); - ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA, - mResizeHandle.getAlpha(), 0f); - mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, resizeAnim); - mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT); - mMenuContainerAnimator.setDuration(animate ? MENU_FADE_DURATION : 0); - mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (animationFinishedRunnable != null) { - animationFinishedRunnable.run(); - } - } - }); - mMenuContainerAnimator.start(); - } - } - - void setActions(Rect stackBounds, List<RemoteAction> actions) { - mActions.clear(); - mActions.addAll(actions); - updateActionViews(stackBounds); - } - - private void updateActionViews(Rect stackBounds) { - ViewGroup expandContainer = findViewById(R.id.expand_container); - ViewGroup actionsContainer = findViewById(R.id.actions_container); - actionsContainer.setOnTouchListener((v, ev) -> { - // Do nothing, prevent click through to parent - return true; - }); - - if (mActions.isEmpty() || mMenuState == MENU_STATE_CLOSE) { - actionsContainer.setVisibility(View.INVISIBLE); - } else { - actionsContainer.setVisibility(View.VISIBLE); - if (mActionsGroup != null) { - // Ensure we have as many buttons as actions - final LayoutInflater inflater = LayoutInflater.from(mContext); - while (mActionsGroup.getChildCount() < mActions.size()) { - final ImageButton actionView = (ImageButton) inflater.inflate( - R.layout.pip_menu_action, mActionsGroup, false); - mActionsGroup.addView(actionView); - } - - // Update the visibility of all views - for (int i = 0; i < mActionsGroup.getChildCount(); i++) { - mActionsGroup.getChildAt(i).setVisibility(i < mActions.size() - ? View.VISIBLE - : View.GONE); - } - - // Recreate the layout - final boolean isLandscapePip = stackBounds != null - && (stackBounds.width() > stackBounds.height()); - for (int i = 0; i < mActions.size(); i++) { - final RemoteAction action = mActions.get(i); - final ImageButton actionView = (ImageButton) mActionsGroup.getChildAt(i); - - // TODO: Check if the action drawable has changed before we reload it - action.getIcon().loadDrawableAsync(mContext, d -> { - d.setTint(Color.WHITE); - actionView.setImageDrawable(d); - }, mHandler); - actionView.setContentDescription(action.getContentDescription()); - if (action.isEnabled()) { - actionView.setOnClickListener(v -> { - mHandler.post(() -> { - try { - action.getActionIntent().send(); - } catch (CanceledException e) { - Log.w(TAG, "Failed to send action", e); - } - }); - }); - } - actionView.setEnabled(action.isEnabled()); - actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); - - // Update the margin between actions - LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) - actionView.getLayoutParams(); - lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0; - } - } - - // Update the expand container margin to adjust the center of the expand button to - // account for the existence of the action container - FrameLayout.LayoutParams expandedLp = - (FrameLayout.LayoutParams) expandContainer.getLayoutParams(); - expandedLp.topMargin = getResources().getDimensionPixelSize( - R.dimen.pip_action_padding); - expandedLp.bottomMargin = getResources().getDimensionPixelSize( - R.dimen.pip_expand_container_edge_margin); - expandContainer.requestLayout(); - } - } - - void updateDismissFraction(float fraction) { - int alpha; - final float menuAlpha = 1 - fraction; - if (mMenuState == MENU_STATE_FULL) { - mMenuContainer.setAlpha(menuAlpha); - mSettingsButton.setAlpha(menuAlpha); - mDismissButton.setAlpha(menuAlpha); - final float interpolatedAlpha = - MENU_BACKGROUND_ALPHA * menuAlpha + DISMISS_BACKGROUND_ALPHA * fraction; - alpha = (int) (interpolatedAlpha * 255); - } else { - if (mMenuState == MENU_STATE_CLOSE) { - mDismissButton.setAlpha(menuAlpha); - } - alpha = (int) (fraction * DISMISS_BACKGROUND_ALPHA * 255); - } - mBackgroundDrawable.setAlpha(alpha); - } - - private void notifyMenuStateChange(int menuState, boolean resize, Runnable callback) { - mMenuState = menuState; - mController.onMenuStateChanged(menuState, resize, callback); - } - - private void expandPip() { - // Do not notify menu visibility when hiding the menu, the controller will do this when it - // handles the message - hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* animate */); - } - - private void dismissPip() { - // Since tapping on the close-button invokes a double-tap wait callback in PipTouchHandler, - // we want to disable animating the fadeout animation of the buttons in order to call on - // PipTouchHandler#onPipDismiss fast enough. - final boolean animate = mMenuState != MENU_STATE_CLOSE; - // Do not notify menu visibility when hiding the menu, the controller will do this when it - // handles the message - hideMenu(mController::onPipDismiss, false /* notifyMenuVisibility */, animate); - } - - private void showSettings() { - final Pair<ComponentName, Integer> topPipActivityInfo = - PipUtils.getTopPipActivity(mContext, ActivityManager.getService()); - if (topPipActivityInfo.first != null) { - final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS, - Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null)); - settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); - mContext.startActivityAsUser(settingsIntent, UserHandle.CURRENT); - } - } - - private void cancelDelayedHide() { - mHandler.removeCallbacks(mHideMenuRunnable); - } - - private void repostDelayedHide(int delay) { - int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay, - FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS); - mHandler.removeCallbacks(mHideMenuRunnable); - mHandler.postDelayed(mHideMenuRunnable, recommendedTimeout); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java deleted file mode 100644 index e24121928808..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java +++ /dev/null @@ -1,665 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.phone; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.ComponentName; -import android.content.Context; -import android.graphics.PointF; -import android.graphics.Rect; -import android.os.Debug; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.view.Choreographer; - -import androidx.dynamicanimation.animation.AnimationHandler; -import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler; -import androidx.dynamicanimation.animation.SpringForce; - -import com.android.systemui.pip.PipSnapAlgorithm; -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.systemui.util.FloatingContentCoordinator; -import com.android.systemui.util.animation.FloatProperties; -import com.android.systemui.util.animation.PhysicsAnimator; -import com.android.systemui.util.magnetictarget.MagnetizedObject; - -import java.io.PrintWriter; -import java.util.function.Consumer; - -import kotlin.Unit; -import kotlin.jvm.functions.Function0; - -/** - * A helper to animate and manipulate the PiP. - */ -public class PipMotionHelper implements PipAppOpsListener.Callback, - FloatingContentCoordinator.FloatingContent { - - private static final String TAG = "PipMotionHelper"; - private static final boolean DEBUG = false; - - private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; - private static final int EXPAND_STACK_TO_MENU_DURATION = 250; - private static final int LEAVE_PIP_DURATION = 300; - private static final int SHIFT_DURATION = 300; - private static final float STASH_RATIO = 0.25f; - - /** Friction to use for PIP when it moves via physics fling animations. */ - private static final float DEFAULT_FRICTION = 2f; - - private final Context mContext; - private final PipTaskOrganizer mPipTaskOrganizer; - - private PipMenuActivityController mMenuController; - private PipSnapAlgorithm mSnapAlgorithm; - - private final Handler mMainHandler = new Handler(Looper.getMainLooper()); - - /** PIP's current bounds on the screen. */ - private final Rect mBounds = new Rect(); - - /** The bounds within which PIP's top-left coordinate is allowed to move. */ - private final Rect mMovementBounds = new Rect(); - - /** The region that all of PIP must stay within. */ - private final Rect mFloatingAllowedArea = new Rect(); - - /** - * Temporary bounds used when PIP is being dragged or animated. These bounds are applied to PIP - * using {@link PipTaskOrganizer#scheduleUserResizePip}, so that we can animate shrinking into - * and expanding out of the magnetic dismiss target. - * - * Once PIP is done being dragged or animated, we set {@link #mBounds} equal to these temporary - * bounds, and call {@link PipTaskOrganizer#scheduleFinishResizePip} to 'officially' move PIP to - * its new bounds. - */ - private final Rect mTemporaryBounds = new Rect(); - - /** The destination bounds to which PIP is animating. */ - private final Rect mAnimatingToBounds = new Rect(); - - /** Coordinator instance for resolving conflicts with other floating content. */ - private FloatingContentCoordinator mFloatingContentCoordinator; - - private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = - ThreadLocal.withInitial(() -> { - FrameCallbackScheduler scheduler = runnable -> - Choreographer.getSfInstance().postFrameCallback(t -> runnable.run()); - AnimationHandler handler = new AnimationHandler(scheduler); - return handler; - }); - - /** - * PhysicsAnimator instance for animating {@link #mTemporaryBounds} using physics animations. - */ - private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( - mTemporaryBounds); - - private MagnetizedObject<Rect> mMagnetizedPip; - - /** - * Update listener that resizes the PIP to {@link #mTemporaryBounds}. - */ - private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener; - - /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */ - private PhysicsAnimator.FlingConfig mFlingConfigX; - private PhysicsAnimator.FlingConfig mFlingConfigY; - /** FlingConfig instances proviced to PhysicsAnimator for stashing. */ - private PhysicsAnimator.FlingConfig mStashConfigX; - - /** SpringConfig to use for fling-then-spring animations. */ - private final PhysicsAnimator.SpringConfig mSpringConfig = - new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); - - /** SpringConfig to use for springing PIP away from conflicting floating content. */ - private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = - new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); - - private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { - mMainHandler.post(() -> { - mMenuController.updateMenuLayout(newBounds); - mBounds.set(newBounds); - }); - }; - - /** - * Whether we're springing to the touch event location (vs. moving it to that position - * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was - * 'stuck' in the target and needs to catch up to the touch location. - */ - private boolean mSpringingToTouch = false; - - /** - * Whether PIP was released in the dismiss target, and will be animated out and dismissed - * shortly. - */ - private boolean mDismissalPending = false; - - /** - * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is - * used to show menu activity when the expand animation is completed. - */ - private Runnable mPostPipTransitionCallback; - - private final PipTaskOrganizer.PipTransitionCallback mPipTransitionCallback = - new PipTaskOrganizer.PipTransitionCallback() { - @Override - public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) {} - - @Override - public void onPipTransitionFinished(ComponentName activity, int direction) { - if (mPostPipTransitionCallback != null) { - mPostPipTransitionCallback.run(); - mPostPipTransitionCallback = null; - } - } - - @Override - public void onPipTransitionCanceled(ComponentName activity, int direction) {} - }; - - public PipMotionHelper(Context context, PipTaskOrganizer pipTaskOrganizer, - PipMenuActivityController menuController, PipSnapAlgorithm snapAlgorithm, - FloatingContentCoordinator floatingContentCoordinator) { - mContext = context; - mPipTaskOrganizer = pipTaskOrganizer; - mMenuController = menuController; - mSnapAlgorithm = snapAlgorithm; - mFloatingContentCoordinator = floatingContentCoordinator; - mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback); - mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler( - mSfAnimationHandlerThreadLocal.get()); - - mResizePipUpdateListener = (target, values) -> { - if (!mTemporaryBounds.isEmpty()) { - mPipTaskOrganizer.scheduleUserResizePip( - mBounds, mTemporaryBounds, null); - } - }; - } - - @NonNull - @Override - public Rect getFloatingBoundsOnScreen() { - return !mAnimatingToBounds.isEmpty() ? mAnimatingToBounds : mBounds; - } - - @NonNull - @Override - public Rect getAllowedFloatingBoundsRegion() { - return mFloatingAllowedArea; - } - - @Override - public void moveToBounds(@NonNull Rect bounds) { - animateToBounds(bounds, mConflictResolutionSpringConfig); - } - - /** - * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations. - */ - void synchronizePinnedStackBounds() { - cancelAnimations(); - mBounds.set(mPipTaskOrganizer.getLastReportedBounds()); - mTemporaryBounds.setEmpty(); - - if (mPipTaskOrganizer.isInPip()) { - mFloatingContentCoordinator.onContentMoved(this); - } - } - - boolean isAnimating() { - return mTemporaryBoundsPhysicsAnimator.isRunning(); - } - - /** - * Tries to move the pinned stack to the given {@param bounds}. - */ - void movePip(Rect toBounds) { - movePip(toBounds, false /* isDragging */); - } - - /** - * Tries to move the pinned stack to the given {@param bounds}. - * - * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we - * won't notify the floating content coordinator of this move, since that will - * happen when the gesture ends. - */ - void movePip(Rect toBounds, boolean isDragging) { - if (!isDragging) { - mFloatingContentCoordinator.onContentMoved(this); - } - - if (!mSpringingToTouch) { - // If we are moving PIP directly to the touch event locations, cancel any animations and - // move PIP to the given bounds. - cancelAnimations(); - - if (!isDragging) { - resizePipUnchecked(toBounds); - mBounds.set(toBounds); - } else { - mTemporaryBounds.set(toBounds); - mPipTaskOrganizer.scheduleUserResizePip(mBounds, mTemporaryBounds, - (Rect newBounds) -> { - mMainHandler.post(() -> { - mMenuController.updateMenuLayout(newBounds); - }); - }); - } - } else { - // If PIP is 'catching up' after being stuck in the dismiss target, update the animation - // to spring towards the new touch location. - mTemporaryBoundsPhysicsAnimator - .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig) - .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig) - .spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig) - .spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig); - - startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */, - false /* dismiss */); - } - } - - /** Animates the PIP into the dismiss target, scaling it down. */ - void animateIntoDismissTarget( - MagnetizedObject.MagneticTarget target, - float velX, float velY, - boolean flung, Function0<Unit> after) { - final PointF targetCenter = target.getCenterOnScreen(); - - final float desiredWidth = mBounds.width() / 2; - final float desiredHeight = mBounds.height() / 2; - - final float destinationX = targetCenter.x - (desiredWidth / 2f); - final float destinationY = targetCenter.y - (desiredHeight / 2f); - - // If we're already in the dismiss target area, then there won't be a move to set the - // temporary bounds, so just initialize it to the current bounds - if (mTemporaryBounds.isEmpty()) { - mTemporaryBounds.set(mBounds); - } - mTemporaryBoundsPhysicsAnimator - .spring(FloatProperties.RECT_X, destinationX, velX, mSpringConfig) - .spring(FloatProperties.RECT_Y, destinationY, velY, mSpringConfig) - .spring(FloatProperties.RECT_WIDTH, desiredWidth, mSpringConfig) - .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mSpringConfig) - .withEndActions(after); - - startBoundsAnimator(destinationX, destinationY, false); - } - - /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */ - void setSpringingToTouch(boolean springingToTouch) { - mSpringingToTouch = springingToTouch; - } - - /** - * Resizes the pinned stack back to unknown windowing mode, which could be freeform or - * * fullscreen depending on the display area's windowing mode. - */ - void expandLeavePip() { - expandLeavePip(false /* skipAnimation */); - } - - /** - * Resizes the pinned stack back to unknown windowing mode, which could be freeform or - * fullscreen depending on the display area's windowing mode. - */ - void expandLeavePip(boolean skipAnimation) { - if (DEBUG) { - Log.d(TAG, "exitPip: skipAnimation=" + skipAnimation - + " callers=\n" + Debug.getCallers(5, " ")); - } - cancelAnimations(); - mMenuController.hideMenuWithoutResize(); - mPipTaskOrganizer.getUpdateHandler().post(() -> { - mPipTaskOrganizer.exitPip(skipAnimation - ? 0 - : LEAVE_PIP_DURATION); - }); - } - - /** - * Dismisses the pinned stack. - */ - @Override - public void dismissPip() { - if (DEBUG) { - Log.d(TAG, "removePip: callers=\n" + Debug.getCallers(5, " ")); - } - cancelAnimations(); - mMenuController.hideMenuWithoutResize(); - mPipTaskOrganizer.removePip(); - } - - /** Sets the movement bounds to use to constrain PIP position animations. */ - void setCurrentMovementBounds(Rect movementBounds) { - mMovementBounds.set(movementBounds); - rebuildFlingConfigs(); - - // The movement bounds represent the area within which we can move PIP's top-left position. - // The allowed area for all of PIP is those bounds plus PIP's width and height. - mFloatingAllowedArea.set(mMovementBounds); - mFloatingAllowedArea.right += mBounds.width(); - mFloatingAllowedArea.bottom += mBounds.height(); - } - - /** - * @return the PiP bounds. - */ - Rect getBounds() { - return mBounds; - } - - /** - * Returns the PIP bounds if we're not animating, or the current, temporary animating bounds - * otherwise. - */ - Rect getPossiblyAnimatingBounds() { - return mTemporaryBounds.isEmpty() ? mBounds : mTemporaryBounds; - } - - /** - * Flings the PiP to the closest snap target. - */ - void flingToSnapTarget( - float velocityX, float velocityY, - @Nullable Runnable updateAction, @Nullable Runnable endAction) { - movetoTarget(velocityX, velocityY, updateAction, endAction, false /* isStash */); - } - - /** - * Stash PiP to the closest edge. - */ - void stashToEdge( - float velocityX, float velocityY, - @Nullable Runnable updateAction, @Nullable Runnable endAction) { - movetoTarget(velocityX, velocityY, updateAction, endAction, true /* isStash */); - } - - private void movetoTarget( - float velocityX, float velocityY, - @Nullable Runnable updateAction, @Nullable Runnable endAction, boolean isStash) { - // If we're flinging to a snap target now, we're not springing to catch up to the touch - // location now. - mSpringingToTouch = false; - - mTemporaryBoundsPhysicsAnimator - .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig) - .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig) - .flingThenSpring( - FloatProperties.RECT_X, velocityX, isStash ? mStashConfigX : mFlingConfigX, - mSpringConfig, true /* flingMustReachMinOrMax */) - .flingThenSpring( - FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig) - .withEndActions(endAction); - - if (updateAction != null) { - mTemporaryBoundsPhysicsAnimator.addUpdateListener( - (target, values) -> updateAction.run()); - } - - final float offset = ((float) mBounds.width()) * (1.0f - STASH_RATIO); - final float leftEdge = isStash ? mMovementBounds.left - offset : mMovementBounds.left; - final float rightEdge = isStash ? mMovementBounds.right + offset : mMovementBounds.right; - - final float xEndValue = velocityX < 0 ? leftEdge : rightEdge; - final float estimatedFlingYEndValue = - PhysicsAnimator.estimateFlingEndValue( - mTemporaryBounds.top, velocityY, mFlingConfigY); - - startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */, - false /* dismiss */); - } - - /** - * Animates PIP to the provided bounds, using physics animations and the given spring - * configuration - */ - void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) { - if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { - // Animate from the current bounds if we're not already animating. - mTemporaryBounds.set(mBounds); - } - - mTemporaryBoundsPhysicsAnimator - .spring(FloatProperties.RECT_X, bounds.left, springConfig) - .spring(FloatProperties.RECT_Y, bounds.top, springConfig); - startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */, - false /* dismiss */); - } - - /** - * Animates the dismissal of the PiP off the edge of the screen. - */ - void animateDismiss() { - // Animate off the bottom of the screen, then dismiss PIP. - mTemporaryBoundsPhysicsAnimator - .spring(FloatProperties.RECT_Y, - mMovementBounds.bottom + mBounds.height() * 2, - 0, - mSpringConfig) - .withEndActions(this::dismissPip); - - startBoundsAnimator( - mBounds.left /* toX */, mBounds.bottom + mBounds.height() /* toY */, - true /* dismiss */); - - mDismissalPending = false; - } - - /** - * Animates the PiP to the expanded state to show the menu. - */ - float animateToExpandedState(Rect expandedBounds, Rect movementBounds, - Rect expandedMovementBounds, Runnable callback) { - float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), movementBounds); - mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction); - mPostPipTransitionCallback = callback; - resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION); - return savedSnapFraction; - } - - /** - * Animates the PiP from the expanded state to the normal state after the menu is hidden. - */ - void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction, - Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) { - if (savedSnapFraction < 0f) { - // If there are no saved snap fractions, then just use the current bounds - savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), - currentMovementBounds); - } - mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction); - - if (immediate) { - movePip(normalBounds); - } else { - resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION); - } - } - - /** - * Animates the PiP to offset it from the IME or shelf. - */ - void animateToOffset(Rect originalBounds, int offset) { - if (DEBUG) { - Log.d(TAG, "animateToOffset: originalBounds=" + originalBounds + " offset=" + offset - + " callers=\n" + Debug.getCallers(5, " ")); - } - cancelAnimations(); - mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, - mUpdateBoundsCallback); - } - - /** - * Cancels all existing animations. - */ - private void cancelAnimations() { - mTemporaryBoundsPhysicsAnimator.cancel(); - mAnimatingToBounds.setEmpty(); - mSpringingToTouch = false; - } - - /** Set new fling configs whose min/max values respect the given movement bounds. */ - private void rebuildFlingConfigs() { - mFlingConfigX = new PhysicsAnimator.FlingConfig( - DEFAULT_FRICTION, mMovementBounds.left, mMovementBounds.right); - mFlingConfigY = new PhysicsAnimator.FlingConfig( - DEFAULT_FRICTION, mMovementBounds.top, mMovementBounds.bottom); - final float offset = ((float) mBounds.width()) * (1.0f - STASH_RATIO); - mStashConfigX = new PhysicsAnimator.FlingConfig( - DEFAULT_FRICTION, mMovementBounds.left - offset, mMovementBounds.right + offset); - } - - /** - * Starts the physics animator which will update the animated PIP bounds using physics - * animations, as well as the TimeAnimator which will apply those bounds to PIP. - * - * This will also add end actions to the bounds animator that cancel the TimeAnimator and update - * the 'real' bounds to equal the final animated bounds. - */ - private void startBoundsAnimator(float toX, float toY, boolean dismiss) { - if (!mSpringingToTouch) { - cancelAnimations(); - } - - // Set animatingToBounds directly to avoid allocating a new Rect, but then call - // setAnimatingToBounds to run the normal logic for changing animatingToBounds. - mAnimatingToBounds.set( - (int) toX, - (int) toY, - (int) toX + mBounds.width(), - (int) toY + mBounds.height()); - setAnimatingToBounds(mAnimatingToBounds); - - if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { - mTemporaryBoundsPhysicsAnimator - .addUpdateListener(mResizePipUpdateListener) - .withEndActions(this::onBoundsAnimationEnd); - } - - mTemporaryBoundsPhysicsAnimator.start(); - } - - /** - * Notify that PIP was released in the dismiss target and will be animated out and dismissed - * shortly. - */ - void notifyDismissalPending() { - mDismissalPending = true; - } - - private void onBoundsAnimationEnd() { - if (!mDismissalPending - && !mSpringingToTouch - && !mMagnetizedPip.getObjectStuckToTarget()) { - mBounds.set(mTemporaryBounds); - if (!mDismissalPending) { - // do not schedule resize if PiP is dismissing, which may cause app re-open to - // mBounds instead of it's normal bounds. - mPipTaskOrganizer.scheduleFinishResizePip(mBounds); - } - mTemporaryBounds.setEmpty(); - } - - mAnimatingToBounds.setEmpty(); - mSpringingToTouch = false; - mDismissalPending = false; - } - - /** - * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so - * we return these bounds from - * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. - */ - private void setAnimatingToBounds(Rect bounds) { - mAnimatingToBounds.set(bounds); - mFloatingContentCoordinator.onContentMoved(this); - } - - /** - * Directly resizes the PiP to the given {@param bounds}. - */ - private void resizePipUnchecked(Rect toBounds) { - if (DEBUG) { - Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds - + " callers=\n" + Debug.getCallers(5, " ")); - } - if (!toBounds.equals(mBounds)) { - mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback); - } - } - - /** - * Directly resizes the PiP to the given {@param bounds}. - */ - private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { - if (DEBUG) { - Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds - + " duration=" + duration + " callers=\n" + Debug.getCallers(5, " ")); - } - - // Intentionally resize here even if the current bounds match the destination bounds. - // This is so all the proper callbacks are performed. - mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, mUpdateBoundsCallback); - setAnimatingToBounds(toBounds); - } - - /** - * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the - * magnetic dismiss target so it can calculate PIP's size and position. - */ - MagnetizedObject<Rect> getMagnetizedPip() { - if (mMagnetizedPip == null) { - mMagnetizedPip = new MagnetizedObject<Rect>( - mContext, mTemporaryBounds, FloatProperties.RECT_X, FloatProperties.RECT_Y) { - @Override - public float getWidth(@NonNull Rect animatedPipBounds) { - return animatedPipBounds.width(); - } - - @Override - public float getHeight(@NonNull Rect animatedPipBounds) { - return animatedPipBounds.height(); - } - - @Override - public void getLocationOnScreen( - @NonNull Rect animatedPipBounds, @NonNull int[] loc) { - loc[0] = animatedPipBounds.left; - loc[1] = animatedPipBounds.top; - } - }; - } - - return mMagnetizedPip; - } - - public void dump(PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.println(prefix + TAG); - pw.println(innerPrefix + "mBounds=" + mBounds); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java deleted file mode 100644 index 08d9b2ae21b0..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java +++ /dev/null @@ -1,528 +0,0 @@ -/* - * Copyright (C) 2020 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.pip.phone; - -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_PINCH_RESIZE; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; -import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; -import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.Region; -import android.hardware.input.InputManager; -import android.os.Handler; -import android.os.Looper; -import android.provider.DeviceConfig; -import android.view.BatchedInputEventReceiver; -import android.view.Choreographer; -import android.view.InputChannel; -import android.view.InputEvent; -import android.view.InputEventReceiver; -import android.view.InputMonitor; -import android.view.MotionEvent; -import android.view.ScaleGestureDetector; -import android.view.ViewConfiguration; - -import com.android.internal.policy.TaskResizingAlgorithm; -import com.android.systemui.model.SysUiState; -import com.android.systemui.pip.PipBoundsHandler; -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.systemui.pip.PipUiEventLogger; -import com.android.systemui.util.DeviceConfigProxy; -import com.android.wm.shell.R; - -import java.io.PrintWriter; -import java.util.concurrent.Executor; -import java.util.function.Function; - -/** - * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to - * trigger dynamic resize. - */ -public class PipResizeGestureHandler { - - private static final String TAG = "PipResizeGestureHandler"; - private static final float PINCH_THRESHOLD = 0.05f; - private static final float STARTING_SCALE_FACTOR = 1.0f; - - private static final int INVALID_SYSUI_STATE_MASK = - SYSUI_STATE_GLOBAL_ACTIONS_SHOWING - | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING - | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED - | SYSUI_STATE_BOUNCER_SHOWING - | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED - | SYSUI_STATE_BUBBLES_EXPANDED - | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; - - private final Context mContext; - private final PipBoundsHandler mPipBoundsHandler; - private final PipMotionHelper mMotionHelper; - private final int mDisplayId; - private final Executor mMainExecutor; - private final SysUiState mSysUiState; - private final ScaleGestureDetector mScaleGestureDetector; - private final Region mTmpRegion = new Region(); - - private final PointF mDownPoint = new PointF(); - private final Point mMaxSize = new Point(); - private final Point mMinSize = new Point(); - private final Rect mLastResizeBounds = new Rect(); - private final Rect mUserResizeBounds = new Rect(); - private final Rect mLastDownBounds = new Rect(); - private final Rect mDragCornerSize = new Rect(); - private final Rect mTmpTopLeftCorner = new Rect(); - private final Rect mTmpTopRightCorner = new Rect(); - private final Rect mTmpBottomLeftCorner = new Rect(); - private final Rect mTmpBottomRightCorner = new Rect(); - private final Rect mDisplayBounds = new Rect(); - private final Function<Rect, Rect> mMovementBoundsSupplier; - private final Runnable mUpdateMovementBoundsRunnable; - - private int mDelta; - private float mTouchSlop; - private boolean mAllowGesture; - private boolean mIsAttached; - private boolean mIsEnabled; - private boolean mEnablePinchResize; - private boolean mThresholdCrossed; - private boolean mUsingPinchToZoom = false; - private float mScaleFactor = STARTING_SCALE_FACTOR; - - private InputMonitor mInputMonitor; - private InputEventReceiver mInputEventReceiver; - private PipTaskOrganizer mPipTaskOrganizer; - private PipMenuActivityController mPipMenuActivityController; - private PipUiEventLogger mPipUiEventLogger; - - private int mCtrlType; - - public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, - PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig, - PipTaskOrganizer pipTaskOrganizer, Function<Rect, Rect> movementBoundsSupplier, - Runnable updateMovementBoundsRunnable, SysUiState sysUiState, - PipUiEventLogger pipUiEventLogger, PipMenuActivityController menuActivityController) { - mContext = context; - mDisplayId = context.getDisplayId(); - mMainExecutor = context.getMainExecutor(); - mPipBoundsHandler = pipBoundsHandler; - mMotionHelper = motionHelper; - mPipTaskOrganizer = pipTaskOrganizer; - mMovementBoundsSupplier = movementBoundsSupplier; - mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; - mSysUiState = sysUiState; - mPipMenuActivityController = menuActivityController; - mPipUiEventLogger = pipUiEventLogger; - - context.getDisplay().getRealSize(mMaxSize); - reloadResources(); - - mScaleGestureDetector = new ScaleGestureDetector(context, - new ScaleGestureDetector.OnScaleGestureListener() { - @Override - public boolean onScale(ScaleGestureDetector detector) { - mScaleFactor *= detector.getScaleFactor(); - - if (!mThresholdCrossed - && (mScaleFactor > (STARTING_SCALE_FACTOR + PINCH_THRESHOLD) - || mScaleFactor < (STARTING_SCALE_FACTOR - PINCH_THRESHOLD))) { - mThresholdCrossed = true; - mInputMonitor.pilferPointers(); - } - if (mThresholdCrossed) { - int height = Math.min(mMaxSize.y, Math.max(mMinSize.y, - (int) (mScaleFactor * mLastDownBounds.height()))); - int width = Math.min(mMaxSize.x, Math.max(mMinSize.x, - (int) (mScaleFactor * mLastDownBounds.width()))); - int top, bottom, left, right; - - if ((mCtrlType & CTRL_TOP) != 0) { - top = mLastDownBounds.bottom - height; - bottom = mLastDownBounds.bottom; - } else { - top = mLastDownBounds.top; - bottom = mLastDownBounds.top + height; - } - - if ((mCtrlType & CTRL_LEFT) != 0) { - left = mLastDownBounds.right - width; - right = mLastDownBounds.right; - } else { - left = mLastDownBounds.left; - right = mLastDownBounds.left + width; - } - - mLastResizeBounds.set(left, top, right, bottom); - mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, - mLastResizeBounds, - null); - } - return true; - } - - @Override - public boolean onScaleBegin(ScaleGestureDetector detector) { - setCtrlTypeForPinchToZoom(); - return true; - } - - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - mScaleFactor = STARTING_SCALE_FACTOR; - finishResize(); - } - }); - - mEnablePinchResize = DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_SYSTEMUI, - PIP_PINCH_RESIZE, - /* defaultValue = */ false); - deviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, mMainExecutor, - new DeviceConfig.OnPropertiesChangedListener() { - @Override - public void onPropertiesChanged(DeviceConfig.Properties properties) { - if (properties.getKeyset().contains(PIP_PINCH_RESIZE)) { - mEnablePinchResize = properties.getBoolean( - PIP_PINCH_RESIZE, /* defaultValue = */ false); - } - } - }); - } - - public void onConfigurationChanged() { - reloadResources(); - } - - private void reloadResources() { - final Resources res = mContext.getResources(); - mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size); - mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); - } - - private void resetDragCorners() { - mDragCornerSize.set(0, 0, mDelta, mDelta); - mTmpTopLeftCorner.set(mDragCornerSize); - mTmpTopRightCorner.set(mDragCornerSize); - mTmpBottomLeftCorner.set(mDragCornerSize); - mTmpBottomRightCorner.set(mDragCornerSize); - } - - private void disposeInputChannel() { - if (mInputEventReceiver != null) { - mInputEventReceiver.dispose(); - mInputEventReceiver = null; - } - if (mInputMonitor != null) { - mInputMonitor.dispose(); - mInputMonitor = null; - } - } - - void onActivityPinned() { - mIsAttached = true; - updateIsEnabled(); - } - - void onActivityUnpinned() { - mIsAttached = false; - mUserResizeBounds.setEmpty(); - updateIsEnabled(); - } - - private void updateIsEnabled() { - boolean isEnabled = mIsAttached; - if (isEnabled == mIsEnabled) { - return; - } - mIsEnabled = isEnabled; - disposeInputChannel(); - - if (mIsEnabled) { - // Register input event receiver - mInputMonitor = InputManager.getInstance().monitorGestureInput( - "pip-resize", mDisplayId); - mInputEventReceiver = new SysUiInputEventReceiver( - mInputMonitor.getInputChannel(), Looper.getMainLooper()); - } - } - - private void onInputEvent(InputEvent ev) { - if (ev instanceof MotionEvent) { - if (mUsingPinchToZoom) { - mScaleGestureDetector.onTouchEvent((MotionEvent) ev); - } else { - onDragCornerResize((MotionEvent) ev); - } - } - } - - /** - * Check whether the current x,y coordinate is within the region in which drag-resize should - * start. - * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which - * overlaps with the PIP window while the rest goes outside of the PIP window. - * _ _ _ _ - * |_|_|_________|_|_| - * |_|_| |_|_| - * | PIP | - * | WINDOW | - * _|_ _|_ - * |_|_|_________|_|_| - * |_|_| |_|_| - */ - public boolean isWithinTouchRegion(int x, int y) { - final Rect currentPipBounds = mMotionHelper.getBounds(); - if (currentPipBounds == null) { - return false; - } - resetDragCorners(); - mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2, - currentPipBounds.top - mDelta / 2); - mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2, - currentPipBounds.top - mDelta / 2); - mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2, - currentPipBounds.bottom - mDelta / 2); - mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2, - currentPipBounds.bottom - mDelta / 2); - - mTmpRegion.setEmpty(); - mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION); - mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION); - mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION); - mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION); - - return mTmpRegion.contains(x, y); - } - - public boolean willStartResizeGesture(MotionEvent ev) { - if (isInValidSysUiState()) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - // Always pass the DOWN event to the ScaleGestureDetector - mScaleGestureDetector.onTouchEvent(ev); - if (isWithinTouchRegion((int) ev.getRawX(), (int) ev.getRawY())) { - return true; - } - break; - - case MotionEvent.ACTION_POINTER_DOWN: - if (mEnablePinchResize && ev.getPointerCount() == 2) { - mUsingPinchToZoom = true; - return true; - } - break; - - default: - break; - } - } - return false; - } - - private void setCtrlTypeForPinchToZoom() { - final Rect currentPipBounds = mMotionHelper.getBounds(); - mLastDownBounds.set(mMotionHelper.getBounds()); - - Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); - mDisplayBounds.set(movementBounds.left, - movementBounds.top, - movementBounds.right + currentPipBounds.width(), - movementBounds.bottom + currentPipBounds.height()); - - if (currentPipBounds.left == mDisplayBounds.left) { - mCtrlType |= CTRL_RIGHT; - } else { - mCtrlType |= CTRL_LEFT; - } - - if (currentPipBounds.top > mDisplayBounds.top + mDisplayBounds.height()) { - mCtrlType |= CTRL_TOP; - } else { - mCtrlType |= CTRL_BOTTOM; - } - } - - private void setCtrlType(int x, int y) { - final Rect currentPipBounds = mMotionHelper.getBounds(); - - Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); - mDisplayBounds.set(movementBounds.left, - movementBounds.top, - movementBounds.right + currentPipBounds.width(), - movementBounds.bottom + currentPipBounds.height()); - - if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top - && currentPipBounds.left != mDisplayBounds.left) { - mCtrlType |= CTRL_LEFT; - mCtrlType |= CTRL_TOP; - } - if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top - && currentPipBounds.right != mDisplayBounds.right) { - mCtrlType |= CTRL_RIGHT; - mCtrlType |= CTRL_TOP; - } - if (mTmpBottomRightCorner.contains(x, y) - && currentPipBounds.bottom != mDisplayBounds.bottom - && currentPipBounds.right != mDisplayBounds.right) { - mCtrlType |= CTRL_RIGHT; - mCtrlType |= CTRL_BOTTOM; - } - if (mTmpBottomLeftCorner.contains(x, y) - && currentPipBounds.bottom != mDisplayBounds.bottom - && currentPipBounds.left != mDisplayBounds.left) { - mCtrlType |= CTRL_LEFT; - mCtrlType |= CTRL_BOTTOM; - } - } - - private boolean isInValidSysUiState() { - return (mSysUiState.getFlags() & INVALID_SYSUI_STATE_MASK) == 0; - } - - private void onDragCornerResize(MotionEvent ev) { - int action = ev.getActionMasked(); - float x = ev.getX(); - float y = ev.getY(); - if (action == MotionEvent.ACTION_DOWN) { - final Rect currentPipBounds = mMotionHelper.getBounds(); - mLastResizeBounds.setEmpty(); - mAllowGesture = isInValidSysUiState() && isWithinTouchRegion((int) x, (int) y); - if (mAllowGesture) { - setCtrlType((int) x, (int) y); - mDownPoint.set(x, y); - mLastDownBounds.set(mMotionHelper.getBounds()); - } - if (!currentPipBounds.contains((int) ev.getX(), (int) ev.getY()) - && mPipMenuActivityController.isMenuVisible()) { - mPipMenuActivityController.hideMenu(); - } - - } else if (mAllowGesture) { - switch (action) { - case MotionEvent.ACTION_POINTER_DOWN: - // We do not support multi touch for resizing via drag - mAllowGesture = false; - break; - case MotionEvent.ACTION_MOVE: - // Capture inputs - if (!mThresholdCrossed - && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) { - mThresholdCrossed = true; - // Reset the down to begin resizing from this point - mDownPoint.set(x, y); - mInputMonitor.pilferPointers(); - } - if (mThresholdCrossed) { - if (mPipMenuActivityController.isMenuVisible()) { - mPipMenuActivityController.hideMenuWithoutResize(); - mPipMenuActivityController.hideMenu(); - } - final Rect currentPipBounds = mMotionHelper.getBounds(); - mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, - mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x, - mMinSize.y, mMaxSize, true, - mLastDownBounds.width() > mLastDownBounds.height())); - mPipBoundsHandler.transformBoundsToAspectRatio(mLastResizeBounds); - mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds, - null); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - finishResize(); - break; - } - } - } - - private void finishResize() { - if (!mLastResizeBounds.isEmpty()) { - mUserResizeBounds.set(mLastResizeBounds); - mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, - (Rect bounds) -> { - new Handler(Looper.getMainLooper()).post(() -> { - mMotionHelper.synchronizePinnedStackBounds(); - mUpdateMovementBoundsRunnable.run(); - resetState(); - }); - }); - mPipUiEventLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); - } else { - resetState(); - } - } - - private void resetState() { - mCtrlType = CTRL_NONE; - mUsingPinchToZoom = false; - mAllowGesture = false; - mThresholdCrossed = false; - } - - void setUserResizeBounds(Rect bounds) { - mUserResizeBounds.set(bounds); - } - - void invalidateUserResizeBounds() { - mUserResizeBounds.setEmpty(); - } - - Rect getUserResizeBounds() { - return mUserResizeBounds; - } - - void updateMaxSize(int maxX, int maxY) { - mMaxSize.set(maxX, maxY); - } - - void updateMinSize(int minX, int minY) { - mMinSize.set(minX, minY); - } - - public void dump(PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.println(prefix + TAG); - pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture); - pw.println(innerPrefix + "mIsAttached=" + mIsAttached); - pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); - pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize); - pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); - } - - class SysUiInputEventReceiver extends BatchedInputEventReceiver { - SysUiInputEventReceiver(InputChannel channel, Looper looper) { - super(channel, looper, Choreographer.getSfInstance()); - } - - public void onInputEvent(InputEvent event) { - PipResizeGestureHandler.this.onInputEvent(event); - finishInputEvent(event, true); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java deleted file mode 100644 index 72335dbed115..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchGesture.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.phone; - -/** - * A generic interface for a touch gesture. - */ -public abstract class PipTouchGesture { - - /** - * Handle the touch down. - */ - public void onDown(PipTouchState touchState) {} - - /** - * Handle the touch move, and return whether the event was consumed. - */ - public boolean onMove(PipTouchState touchState) { - return false; - } - - /** - * Handle the touch up, and return whether the gesture was consumed. - */ - public boolean onUp(PipTouchState touchState) { - return false; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java deleted file mode 100644 index 858683c4e2d4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ /dev/null @@ -1,1115 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.phone; - -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING; -import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; -import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE; -import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL; -import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE; - -import android.annotation.SuppressLint; -import android.app.IActivityManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.PixelFormat; -import android.graphics.Point; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.drawable.TransitionDrawable; -import android.os.Handler; -import android.os.RemoteException; -import android.provider.DeviceConfig; -import android.util.Log; -import android.util.Size; -import android.view.Gravity; -import android.view.IPinnedStackController; -import android.view.InputEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityWindowInfo; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.SpringForce; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.R; -import com.android.systemui.model.SysUiState; -import com.android.systemui.pip.PipAnimationController; -import com.android.systemui.pip.PipBoundsHandler; -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.systemui.pip.PipUiEventLogger; -import com.android.systemui.shared.system.InputConsumerController; -import com.android.systemui.util.DeviceConfigProxy; -import com.android.systemui.util.DismissCircleView; -import com.android.systemui.util.FloatingContentCoordinator; -import com.android.systemui.util.animation.PhysicsAnimator; -import com.android.systemui.util.magnetictarget.MagnetizedObject; - -import java.io.PrintWriter; - -import kotlin.Unit; - -/** - * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding - * the PIP. - */ -public class PipTouchHandler { - private static final String TAG = "PipTouchHandler"; - - /** Duration of the dismiss scrim fading in/out. */ - private static final int DISMISS_TRANSITION_DURATION_MS = 200; - - /* The multiplier to apply scale the target size by when applying the magnetic field radius */ - private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; - - // Allow dragging the PIP to a location to close it - private final boolean mEnableDismissDragToEdge; - // Allow PIP to resize to a slightly bigger state upon touch - private final boolean mEnableResize; - private final Context mContext; - private final WindowManager mWindowManager; - private final IActivityManager mActivityManager; - private final PipBoundsHandler mPipBoundsHandler; - private final PipUiEventLogger mPipUiEventLogger; - - private PipResizeGestureHandler mPipResizeGestureHandler; - private IPinnedStackController mPinnedStackController; - - private final PipMenuActivityController mMenuController; - private final AccessibilityManager mAccessibilityManager; - private boolean mShowPipMenuOnAnimationEnd = false; - - /** - * Whether PIP stash is enabled or not. When enabled, if at the time of fling-release the - * PIP bounds is outside the left/right edge of the screen, it will be shown in "stashed" mode, - * where PIP will only show partially. - */ - private boolean mEnableStash = false; - - /** - * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move - * PIP. - */ - private MagnetizedObject<Rect> mMagnetizedPip; - - /** - * Container for the dismiss circle, so that it can be animated within the container via - * translation rather than within the WindowManager via slow layout animations. - */ - private ViewGroup mTargetViewContainer; - - /** Circle view used to render the dismiss target. */ - private DismissCircleView mTargetView; - - /** - * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius. - */ - private MagnetizedObject.MagneticTarget mMagneticTarget; - - /** PhysicsAnimator instance for animating the dismiss target in/out. */ - private PhysicsAnimator<View> mMagneticTargetAnimator; - - /** Default configuration to use for springing the dismiss target in/out. */ - private final PhysicsAnimator.SpringConfig mTargetSpringConfig = - new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); - - // The current movement bounds - private Rect mMovementBounds = new Rect(); - - // The reference inset bounds, used to determine the dismiss fraction - private Rect mInsetBounds = new Rect(); - // The reference bounds used to calculate the normal/expanded target bounds - private Rect mNormalBounds = new Rect(); - @VisibleForTesting Rect mNormalMovementBounds = new Rect(); - private Rect mExpandedBounds = new Rect(); - @VisibleForTesting Rect mExpandedMovementBounds = new Rect(); - private int mExpandedShortestEdgeSize; - - // Used to workaround an issue where the WM rotation happens before we are notified, allowing - // us to send stale bounds - private int mDeferResizeToNormalBoundsUntilRotation = -1; - private int mDisplayRotation; - - /** - * Runnable that can be posted delayed to show the target. This needs to be saved as a member - * variable so we can pass it to removeCallbacks. - */ - private Runnable mShowTargetAction = this::showDismissTargetMaybe; - - private Handler mHandler = new Handler(); - - // Behaviour states - private int mMenuState = MENU_STATE_NONE; - private boolean mIsImeShowing; - private int mImeHeight; - private int mImeOffset; - private int mDismissAreaHeight; - private boolean mIsShelfShowing; - private int mShelfHeight; - private int mMovementBoundsExtraOffsets; - private int mBottomOffsetBufferPx; - private float mSavedSnapFraction = -1f; - private boolean mSendingHoverAccessibilityEvents; - private boolean mMovementWithinDismiss; - private PipAccessibilityInteractionConnection mConnection; - - // Touch state - private final PipTouchState mTouchState; - private final FloatingContentCoordinator mFloatingContentCoordinator; - private PipMotionHelper mMotionHelper; - private PipTouchGesture mGesture; - - // Temp vars - private final Rect mTmpBounds = new Rect(); - - /** - * A listener for the PIP menu activity. - */ - private class PipMenuListener implements PipMenuActivityController.Listener { - @Override - public void onPipMenuStateChanged(int menuState, boolean resize, Runnable callback) { - setMenuState(menuState, resize, callback); - } - - @Override - public void onPipExpand() { - mMotionHelper.expandLeavePip(); - } - - @Override - public void onPipDismiss() { - mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); - mTouchState.removeDoubleTapTimeoutCallback(); - mMotionHelper.dismissPip(); - } - - @Override - public void onPipShowMenu() { - mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), - true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()); - } - } - - @SuppressLint("InflateParams") - public PipTouchHandler(Context context, IActivityManager activityManager, - PipMenuActivityController menuController, - InputConsumerController inputConsumerController, - PipBoundsHandler pipBoundsHandler, - PipTaskOrganizer pipTaskOrganizer, - FloatingContentCoordinator floatingContentCoordinator, - DeviceConfigProxy deviceConfig, - SysUiState sysUiState, - PipUiEventLogger pipUiEventLogger) { - // Initialize the Pip input consumer - mContext = context; - mActivityManager = activityManager; - mAccessibilityManager = context.getSystemService(AccessibilityManager.class); - mPipBoundsHandler = pipBoundsHandler; - mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); - mMenuController = menuController; - mMenuController.addListener(new PipMenuListener()); - mGesture = new DefaultPipTouchGesture(); - mMotionHelper = new PipMotionHelper(mContext, pipTaskOrganizer, mMenuController, - mPipBoundsHandler.getSnapAlgorithm(), floatingContentCoordinator); - mPipResizeGestureHandler = - new PipResizeGestureHandler(context, pipBoundsHandler, mMotionHelper, - deviceConfig, pipTaskOrganizer, this::getMovementBounds, - this::updateMovementBounds, sysUiState, pipUiEventLogger, menuController); - mTouchState = new PipTouchState(ViewConfiguration.get(context), mHandler, - () -> mMenuController.showMenuWithDelay(MENU_STATE_FULL, mMotionHelper.getBounds(), - true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()), - menuController::hideMenu); - - Resources res = context.getResources(); - mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge); - mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); - reloadResources(); - - // Register the listener for input consumer touch events - inputConsumerController.setInputListener(this::handleTouchEvent); - inputConsumerController.setRegistrationListener(this::onRegistrationChanged); - - mFloatingContentCoordinator = floatingContentCoordinator; - mConnection = new PipAccessibilityInteractionConnection(mContext, mMotionHelper, - pipTaskOrganizer, mPipBoundsHandler.getSnapAlgorithm(), - this::onAccessibilityShowMenu, this::updateMovementBounds, mHandler); - - mPipUiEventLogger = pipUiEventLogger; - - mTargetView = new DismissCircleView(context); - mTargetViewContainer = new FrameLayout(context); - mTargetViewContainer.setBackgroundDrawable( - context.getDrawable(R.drawable.floating_dismiss_gradient_transition)); - mTargetViewContainer.setClipChildren(false); - mTargetViewContainer.addView(mTargetView); - - mMagnetizedPip = mMotionHelper.getMagnetizedPip(); - mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); - updateMagneticTargetSize(); - - mMagnetizedPip.setAnimateStuckToTarget( - (target, velX, velY, flung, after) -> { - if (mEnableDismissDragToEdge) { - mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after); - } - return Unit.INSTANCE; - }); - mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { - @Override - public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { - // Show the dismiss target, in case the initial touch event occurred within the - // magnetic field radius. - if (mEnableDismissDragToEdge) { - showDismissTargetMaybe(); - } - } - - @Override - public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, - float velX, float velY, boolean wasFlungOut) { - if (wasFlungOut) { - mMotionHelper.flingToSnapTarget(velX, velY, null, null); - hideDismissTarget(); - } else { - mMotionHelper.setSpringingToTouch(true); - } - } - - @Override - public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - mMotionHelper.notifyDismissalPending(); - - mHandler.post(() -> { - mMotionHelper.animateDismiss(); - hideDismissTarget(); - }); - - mPipUiEventLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); - } - }); - - mMagneticTargetAnimator = PhysicsAnimator.getInstance(mTargetView); - - mEnableStash = DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_SYSTEMUI, - PIP_STASHING, - /* defaultValue = */ false); - deviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, - context.getMainExecutor(), - new DeviceConfig.OnPropertiesChangedListener() { - @Override - public void onPropertiesChanged(DeviceConfig.Properties properties) { - if (properties.getKeyset().contains(PIP_STASHING)) { - mEnableStash = properties.getBoolean( - PIP_STASHING, /* defaultValue = */ false); - } - } - }); - } - - private void reloadResources() { - final Resources res = mContext.getResources(); - mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer); - mExpandedShortestEdgeSize = res.getDimensionPixelSize( - R.dimen.pip_expanded_shortest_edge_size); - mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); - mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); - updateMagneticTargetSize(); - } - - private void updateMagneticTargetSize() { - if (mTargetView == null) { - return; - } - - final Resources res = mContext.getResources(); - final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); - final FrameLayout.LayoutParams newParams = - new FrameLayout.LayoutParams(targetSize, targetSize); - newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; - newParams.bottomMargin = mContext.getResources().getDimensionPixelSize( - R.dimen.floating_dismiss_bottom_margin); - mTargetView.setLayoutParams(newParams); - - // Set the magnetic field radius equal to the target size from the center of the target - mMagneticTarget.setMagneticFieldRadiusPx( - (int) (targetSize * MAGNETIC_FIELD_RADIUS_MULTIPLIER)); - } - - private boolean shouldShowResizeHandle() { - return false; - } - - public void setTouchGesture(PipTouchGesture gesture) { - mGesture = gesture; - } - - public void setTouchEnabled(boolean enabled) { - mTouchState.setAllowTouches(enabled); - } - - public void showPictureInPictureMenu() { - // Only show the menu if the user isn't currently interacting with the PiP - if (!mTouchState.isUserInteracting()) { - mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), - false /* allowMenuTimeout */, willResizeMenu(), - shouldShowResizeHandle()); - } - } - - public void onActivityPinned() { - createOrUpdateDismissTarget(); - - mShowPipMenuOnAnimationEnd = true; - mPipResizeGestureHandler.onActivityPinned(); - mFloatingContentCoordinator.onContentAdded(mMotionHelper); - } - - public void onActivityUnpinned(ComponentName topPipActivity) { - if (topPipActivity == null) { - // Clean up state after the last PiP activity is removed - cleanUpDismissTarget(); - - mFloatingContentCoordinator.onContentRemoved(mMotionHelper); - } - mPipResizeGestureHandler.onActivityUnpinned(); - } - - public void onPinnedStackAnimationEnded( - @PipAnimationController.TransitionDirection int direction) { - // Always synchronize the motion helper bounds once PiP animations finish - mMotionHelper.synchronizePinnedStackBounds(); - updateMovementBounds(); - if (direction == TRANSITION_DIRECTION_TO_PIP) { - // Set the initial bounds as the user resize bounds. - mPipResizeGestureHandler.setUserResizeBounds(mMotionHelper.getBounds()); - } - - if (mShowPipMenuOnAnimationEnd) { - mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(), - true /* allowMenuTimeout */, false /* willResizeMenu */, - shouldShowResizeHandle()); - mShowPipMenuOnAnimationEnd = false; - } - } - - public void onConfigurationChanged() { - mPipResizeGestureHandler.onConfigurationChanged(); - mMotionHelper.synchronizePinnedStackBounds(); - reloadResources(); - - // Recreate the dismiss target for the new orientation. - createOrUpdateDismissTarget(); - } - - public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { - mIsImeShowing = imeVisible; - mImeHeight = imeHeight; - } - - public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { - mIsShelfShowing = shelfVisible; - mShelfHeight = shelfHeight; - } - - public void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) { - final Rect toMovementBounds = new Rect(); - mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(outBounds, insetBounds, - toMovementBounds, 0); - final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets; - if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) { - outBounds.offsetTo(outBounds.left, toMovementBounds.bottom); - } - } - - /** - * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window. - */ - public void onAspectRatioChanged() { - mPipResizeGestureHandler.invalidateUserResizeBounds(); - } - - public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, - boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) { - // Set the user resized bounds equal to the new normal bounds in case they were - // invalidated (e.g. by an aspect ratio change). - if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) { - mPipResizeGestureHandler.setUserResizeBounds(normalBounds); - } - - final int bottomOffset = mIsImeShowing ? mImeHeight : 0; - final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation); - if (fromDisplayRotationChanged) { - mTouchState.reset(); - } - - // Re-calculate the expanded bounds - mNormalBounds.set(normalBounds); - Rect normalMovementBounds = new Rect(); - mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mNormalBounds, insetBounds, - normalMovementBounds, bottomOffset); - - if (mMovementBounds.isEmpty()) { - // mMovementBounds is not initialized yet and a clean movement bounds without - // bottom offset shall be used later in this function. - mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, insetBounds, - mMovementBounds, 0 /* bottomOffset */); - } - - // Calculate the expanded size - float aspectRatio = (float) normalBounds.width() / normalBounds.height(); - Point displaySize = new Point(); - mContext.getDisplay().getRealSize(displaySize); - Size expandedSize = mPipBoundsHandler.getSnapAlgorithm().getSizeForAspectRatio(aspectRatio, - mExpandedShortestEdgeSize, displaySize.x, displaySize.y); - mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight()); - Rect expandedMovementBounds = new Rect(); - mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mExpandedBounds, insetBounds, - expandedMovementBounds, bottomOffset); - - mPipResizeGestureHandler.updateMinSize(mNormalBounds.width(), mNormalBounds.height()); - mPipResizeGestureHandler.updateMaxSize(mExpandedBounds.width(), mExpandedBounds.height()); - - // The extra offset does not really affect the movement bounds, but are applied based on the - // current state (ime showing, or shelf offset) when we need to actually shift - int extraOffset = Math.max( - mIsImeShowing ? mImeOffset : 0, - !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0); - - // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not - // occluded by the IME or shelf. - if (fromImeAdjustment || fromShelfAdjustment) { - if (mTouchState.isUserInteracting()) { - // Defer the update of the current movement bounds until after the user finishes - // touching the screen - } else { - final boolean isExpanded = mMenuState == MENU_STATE_FULL && willResizeMenu(); - final Rect toMovementBounds = new Rect(); - mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, insetBounds, - toMovementBounds, mIsImeShowing ? mImeHeight : 0); - final int prevBottom = mMovementBounds.bottom - mMovementBoundsExtraOffsets; - // This is to handle landscape fullscreen IMEs, don't apply the extra offset in this - // case - final int toBottom = toMovementBounds.bottom < toMovementBounds.top - ? toMovementBounds.bottom - : toMovementBounds.bottom - extraOffset; - - if (isExpanded) { - curBounds.set(mExpandedBounds); - mPipBoundsHandler.getSnapAlgorithm().applySnapFraction(curBounds, - toMovementBounds, mSavedSnapFraction); - } - - if (prevBottom < toBottom) { - // The movement bounds are expanding - if (curBounds.top > prevBottom - mBottomOffsetBufferPx) { - mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top); - } - } else if (prevBottom > toBottom) { - // The movement bounds are shrinking - if (curBounds.top > toBottom - mBottomOffsetBufferPx) { - mMotionHelper.animateToOffset(curBounds, toBottom - curBounds.top); - } - } - } - } - - // Update the movement bounds after doing the calculations based on the old movement bounds - // above - mNormalMovementBounds.set(normalMovementBounds); - mExpandedMovementBounds.set(expandedMovementBounds); - mDisplayRotation = displayRotation; - mInsetBounds.set(insetBounds); - updateMovementBounds(); - mMovementBoundsExtraOffsets = extraOffset; - mConnection.onMovementBoundsChanged(mNormalBounds, mExpandedBounds, mNormalMovementBounds, - mExpandedMovementBounds); - - // If we have a deferred resize, apply it now - if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { - mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, - mNormalMovementBounds, mMovementBounds, true /* immediate */); - mSavedSnapFraction = -1f; - mDeferResizeToNormalBoundsUntilRotation = -1; - } - } - - /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ - private void createOrUpdateDismissTarget() { - if (!mTargetViewContainer.isAttachedToWindow()) { - mHandler.removeCallbacks(mShowTargetAction); - mMagneticTargetAnimator.cancel(); - - mTargetViewContainer.setVisibility(View.INVISIBLE); - - try { - mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); - } catch (IllegalStateException e) { - // This shouldn't happen, but if the target is already added, just update its layout - // params. - mWindowManager.updateViewLayout( - mTargetViewContainer, getDismissTargetLayoutParams()); - } - } else { - mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams()); - } - } - - /** Returns layout params for the dismiss target, using the latest display metrics. */ - private WindowManager.LayoutParams getDismissTargetLayoutParams() { - final Point windowSize = new Point(); - mWindowManager.getDefaultDisplay().getRealSize(windowSize); - - final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( - WindowManager.LayoutParams.MATCH_PARENT, - mDismissAreaHeight, - 0, windowSize.y - mDismissAreaHeight, - WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT); - - lp.setTitle("pip-dismiss-overlay"); - lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - lp.setFitInsetsTypes(0 /* types */); - - return lp; - } - - /** Makes the dismiss target visible and animates it in, if it isn't already visible. */ - private void showDismissTargetMaybe() { - createOrUpdateDismissTarget(); - - if (mTargetViewContainer.getVisibility() != View.VISIBLE) { - - mTargetView.setTranslationY(mTargetViewContainer.getHeight()); - mTargetViewContainer.setVisibility(View.VISIBLE); - - // Cancel in case we were in the middle of animating it out. - mMagneticTargetAnimator.cancel(); - mMagneticTargetAnimator - .spring(DynamicAnimation.TRANSLATION_Y, 0f, mTargetSpringConfig) - .start(); - - ((TransitionDrawable) mTargetViewContainer.getBackground()).startTransition( - DISMISS_TRANSITION_DURATION_MS); - } - } - - /** Animates the magnetic dismiss target out and then sets it to GONE. */ - private void hideDismissTarget() { - mHandler.removeCallbacks(mShowTargetAction); - mMagneticTargetAnimator - .spring(DynamicAnimation.TRANSLATION_Y, - mTargetViewContainer.getHeight(), - mTargetSpringConfig) - .withEndActions(() -> mTargetViewContainer.setVisibility(View.GONE)) - .start(); - - ((TransitionDrawable) mTargetViewContainer.getBackground()).reverseTransition( - DISMISS_TRANSITION_DURATION_MS); - } - - /** - * Removes the dismiss target and cancels any pending callbacks to show it. - */ - private void cleanUpDismissTarget() { - mHandler.removeCallbacks(mShowTargetAction); - - if (mTargetViewContainer.isAttachedToWindow()) { - mWindowManager.removeViewImmediate(mTargetViewContainer); - } - } - - private void onRegistrationChanged(boolean isRegistered) { - mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered - ? mConnection : null); - if (!isRegistered && mTouchState.isUserInteracting()) { - // If the input consumer is unregistered while the user is interacting, then we may not - // get the final TOUCH_UP event, so clean up the dismiss target as well - cleanUpDismissTarget(); - } - } - - private void onAccessibilityShowMenu() { - mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), - true /* allowMenuTimeout */, willResizeMenu(), - shouldShowResizeHandle()); - } - - private boolean handleTouchEvent(InputEvent inputEvent) { - // Skip any non motion events - if (!(inputEvent instanceof MotionEvent)) { - return true; - } - // Skip touch handling until we are bound to the controller - if (mPinnedStackController == null) { - return true; - } - - MotionEvent ev = (MotionEvent) inputEvent; - if (mPipResizeGestureHandler.willStartResizeGesture(ev)) { - // Initialize the touch state for the gesture, but immediately reset to invalidate the - // gesture - mTouchState.onTouchEvent(ev); - mTouchState.reset(); - return true; - } - - if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting()) - && mMagnetizedPip.maybeConsumeMotionEvent(ev)) { - // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event - // to the touch state. Touch state needs a DOWN event in order to later process MOVE - // events it'll receive if the object is dragged out of the magnetic field. - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mTouchState.onTouchEvent(ev); - } - - // Continue tracking velocity when the object is in the magnetic field, since we want to - // respect touch input velocity if the object is dragged out and then flung. - mTouchState.addMovementToVelocityTracker(ev); - - return true; - } - - // Update the touch state - mTouchState.onTouchEvent(ev); - - boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE; - - switch (ev.getAction()) { - case MotionEvent.ACTION_DOWN: { - mGesture.onDown(mTouchState); - break; - } - case MotionEvent.ACTION_MOVE: { - if (mGesture.onMove(mTouchState)) { - break; - } - - shouldDeliverToMenu = !mTouchState.isDragging(); - break; - } - case MotionEvent.ACTION_UP: { - // Update the movement bounds again if the state has changed since the user started - // dragging (ie. when the IME shows) - updateMovementBounds(); - - if (mGesture.onUp(mTouchState)) { - break; - } - - // Fall through to clean up - } - case MotionEvent.ACTION_CANCEL: { - shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging(); - mTouchState.reset(); - break; - } - case MotionEvent.ACTION_HOVER_ENTER: - // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably - // on and changing MotionEvents into HoverEvents. - // Let's not enable menu show/hide for a11y services. - if (!mAccessibilityManager.isTouchExplorationEnabled()) { - mTouchState.removeHoverExitTimeoutCallback(); - mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), - false /* allowMenuTimeout */, false /* willResizeMenu */, - shouldShowResizeHandle()); - } - case MotionEvent.ACTION_HOVER_MOVE: { - if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) { - sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); - mSendingHoverAccessibilityEvents = true; - } - break; - } - case MotionEvent.ACTION_HOVER_EXIT: { - // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably - // on and changing MotionEvents into HoverEvents. - // Let's not enable menu show/hide for a11y services. - if (!mAccessibilityManager.isTouchExplorationEnabled()) { - mTouchState.scheduleHoverExitTimeoutCallback(); - } - if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) { - sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); - mSendingHoverAccessibilityEvents = false; - } - break; - } - } - - // Deliver the event to PipMenuActivity to handle button click if the menu has shown. - if (shouldDeliverToMenu) { - final MotionEvent cloneEvent = MotionEvent.obtain(ev); - // Send the cancel event and cancel menu timeout if it starts to drag. - if (mTouchState.startedDragging()) { - cloneEvent.setAction(MotionEvent.ACTION_CANCEL); - mMenuController.pokeMenu(); - } - - mMenuController.handlePointerEvent(cloneEvent); - } - - return true; - } - - private void sendAccessibilityHoverEvent(int type) { - if (!mAccessibilityManager.isEnabled()) { - return; - } - - AccessibilityEvent event = AccessibilityEvent.obtain(type); - event.setImportantForAccessibility(true); - event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); - event.setWindowId( - AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); - mAccessibilityManager.sendAccessibilityEvent(event); - } - - /** - * Updates the appearance of the menu and scrim on top of the PiP while dismissing. - */ - private void updateDismissFraction() { - if (mMenuController != null) { - Rect bounds = mMotionHelper.getBounds(); - final float target = mInsetBounds.bottom; - float fraction = 0f; - if (bounds.bottom > target) { - final float distance = bounds.bottom - target; - fraction = Math.min(distance / bounds.height(), 1f); - } - if (Float.compare(fraction, 0f) != 0 || mMenuController.isMenuVisible()) { - // Update if the fraction > 0, or if fraction == 0 and the menu was already visible - mMenuController.setDismissFraction(fraction); - } - } - } - - /** - * Sets the controller to update the system of changes from user interaction. - */ - void setPinnedStackController(IPinnedStackController controller) { - mPinnedStackController = controller; - } - - /** - * Sets the menu visibility. - */ - private void setMenuState(int menuState, boolean resize, Runnable callback) { - if (mMenuState == menuState && !resize) { - return; - } - - if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) { - // Save the current snap fraction and if we do not drag or move the PiP, then - // we store back to this snap fraction. Otherwise, we'll reset the snap - // fraction and snap to the closest edge. - if (resize) { - Rect expandedBounds = new Rect(mExpandedBounds); - mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds, - mMovementBounds, mExpandedMovementBounds, callback); - } - } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) { - // Try and restore the PiP to the closest edge, using the saved snap fraction - // if possible - if (resize) { - if (mDeferResizeToNormalBoundsUntilRotation == -1) { - // This is a very special case: when the menu is expanded and visible, - // navigating to another activity can trigger auto-enter PiP, and if the - // revealed activity has a forced rotation set, then the controller will get - // updated with the new rotation of the display. However, at the same time, - // SystemUI will try to hide the menu by creating an animation to the normal - // bounds which are now stale. In such a case we defer the animation to the - // normal bounds until after the next onMovementBoundsChanged() call to get the - // bounds in the new orientation - try { - int displayRotation = mPinnedStackController.getDisplayRotation(); - if (mDisplayRotation != displayRotation) { - mDeferResizeToNormalBoundsUntilRotation = displayRotation; - } - } catch (RemoteException e) { - Log.e(TAG, "Could not get display rotation from controller"); - } - } - - if (mDeferResizeToNormalBoundsUntilRotation == -1) { - Rect restoreBounds = new Rect(getUserResizeBounds()); - Rect restoredMovementBounds = new Rect(); - mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(restoreBounds, - mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); - mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction, - restoredMovementBounds, mMovementBounds, false /* immediate */); - mSavedSnapFraction = -1f; - } - } else { - mSavedSnapFraction = -1f; - } - } - mMenuState = menuState; - updateMovementBounds(); - // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip - // as well, or it can't handle a11y focus and pip menu can't perform any action. - onRegistrationChanged(menuState == MENU_STATE_NONE); - if (menuState == MENU_STATE_NONE) { - mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU); - } else if (menuState == MENU_STATE_FULL) { - mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU); - } - } - - /** - * @return the motion helper. - */ - public PipMotionHelper getMotionHelper() { - return mMotionHelper; - } - - @VisibleForTesting - PipResizeGestureHandler getPipResizeGestureHandler() { - return mPipResizeGestureHandler; - } - - @VisibleForTesting - void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) { - mPipResizeGestureHandler = pipResizeGestureHandler; - } - - @VisibleForTesting - void setPipMotionHelper(PipMotionHelper pipMotionHelper) { - mMotionHelper = pipMotionHelper; - } - - /** - * @return the unexpanded bounds. - */ - public Rect getNormalBounds() { - return mNormalBounds; - } - - Rect getUserResizeBounds() { - return mPipResizeGestureHandler.getUserResizeBounds(); - } - - /** - * Gesture controlling normal movement of the PIP. - */ - private class DefaultPipTouchGesture extends PipTouchGesture { - private final Point mStartPosition = new Point(); - private final PointF mDelta = new PointF(); - private boolean mShouldHideMenuAfterFling; - - @Override - public void onDown(PipTouchState touchState) { - if (!touchState.isUserInteracting()) { - return; - } - - Rect bounds = mMotionHelper.getPossiblyAnimatingBounds(); - mDelta.set(0f, 0f); - mStartPosition.set(bounds.left, bounds.top); - mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom; - mMotionHelper.setSpringingToTouch(false); - - // If the menu is still visible then just poke the menu - // so that it will timeout after the user stops touching it - if (mMenuState != MENU_STATE_NONE) { - mMenuController.pokeMenu(); - } - } - - @Override - public boolean onMove(PipTouchState touchState) { - if (!touchState.isUserInteracting()) { - return false; - } - - if (touchState.startedDragging()) { - mSavedSnapFraction = -1f; - - if (mEnableDismissDragToEdge) { - if (mTargetViewContainer.getVisibility() != View.VISIBLE) { - mHandler.removeCallbacks(mShowTargetAction); - showDismissTargetMaybe(); - } - } - } - - if (touchState.isDragging()) { - // Move the pinned stack freely - final PointF lastDelta = touchState.getLastTouchDelta(); - float lastX = mStartPosition.x + mDelta.x; - float lastY = mStartPosition.y + mDelta.y; - float left = lastX + lastDelta.x; - float top = lastY + lastDelta.y; - - // Add to the cumulative delta after bounding the position - mDelta.x += left - lastX; - mDelta.y += top - lastY; - - mTmpBounds.set(mMotionHelper.getPossiblyAnimatingBounds()); - mTmpBounds.offsetTo((int) left, (int) top); - mMotionHelper.movePip(mTmpBounds, true /* isDragging */); - - final PointF curPos = touchState.getLastTouchPosition(); - if (mMovementWithinDismiss) { - // Track if movement remains near the bottom edge to identify swipe to dismiss - mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom; - } - return true; - } - return false; - } - - @Override - public boolean onUp(PipTouchState touchState) { - if (mEnableDismissDragToEdge) { - hideDismissTarget(); - } - - if (!touchState.isUserInteracting()) { - return false; - } - - final PointF vel = touchState.getVelocity(); - - if (touchState.isDragging()) { - if (mMenuState != MENU_STATE_NONE) { - // If the menu is still visible, then just poke the menu so that - // it will timeout after the user stops touching it - mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(), - true /* allowMenuTimeout */, willResizeMenu(), - shouldShowResizeHandle()); - } - mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE; - - // Reset the touch state on up before the fling settles - mTouchState.reset(); - final Rect animatingBounds = mMotionHelper.getPossiblyAnimatingBounds(); - // If User releases the PIP window while it's out of the display bounds, put - // PIP into stashed mode. - if (mEnableStash - && (animatingBounds.right > mPipBoundsHandler.getDisplayBounds().right - || animatingBounds.left < mPipBoundsHandler.getDisplayBounds().left)) { - mMotionHelper.stashToEdge(vel.x, vel.y, - PipTouchHandler.this::updateDismissFraction /* updateAction */, - this::flingEndAction /* endAction */); - } else { - mMotionHelper.flingToSnapTarget(vel.x, vel.y, - PipTouchHandler.this::updateDismissFraction /* updateAction */, - this::flingEndAction /* endAction */); - } - } else if (mTouchState.isDoubleTap()) { - // Expand to fullscreen if this is a double tap - // the PiP should be frozen until the transition ends - setTouchEnabled(false); - mMotionHelper.expandLeavePip(); - } else if (mMenuState != MENU_STATE_FULL) { - if (!mTouchState.isWaitingForDoubleTap()) { - // User has stalled long enough for this not to be a drag or a double tap, just - // expand the menu - mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(), - true /* allowMenuTimeout */, willResizeMenu(), - shouldShowResizeHandle()); - } else { - // Next touch event _may_ be the second tap for the double-tap, schedule a - // fallback runnable to trigger the menu if no touch event occurs before the - // next tap - mTouchState.scheduleDoubleTapTimeoutCallback(); - } - } - return true; - } - - private void flingEndAction() { - if (mShouldHideMenuAfterFling) { - // If the menu is not visible, then we can still be showing the activity for the - // dismiss overlay, so just finish it after the animation completes - mMenuController.hideMenu(); - } - } - } - - /** - * Updates the current movement bounds based on whether the menu is currently visible and - * resized. - */ - private void updateMovementBounds() { - mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(mMotionHelper.getBounds(), - mInsetBounds, mMovementBounds, mIsImeShowing ? mImeHeight : 0); - mMotionHelper.setCurrentMovementBounds(mMovementBounds); - - boolean isMenuExpanded = mMenuState == MENU_STATE_FULL; - mPipBoundsHandler.setMinEdgeSize( - isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize : 0); - } - - private Rect getMovementBounds(Rect curBounds) { - Rect movementBounds = new Rect(); - mPipBoundsHandler.getSnapAlgorithm().getMovementBounds(curBounds, mInsetBounds, - movementBounds, mIsImeShowing ? mImeHeight : 0); - return movementBounds; - } - - /** - * @return whether the menu will resize as a part of showing the full menu. - */ - private boolean willResizeMenu() { - if (!mEnableResize) { - return false; - } - return mExpandedBounds.width() != mNormalBounds.width() - || mExpandedBounds.height() != mNormalBounds.height(); - } - - public void dump(PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.println(prefix + TAG); - pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds); - pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds); - pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds); - pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds); - pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); - pw.println(innerPrefix + "mMenuState=" + mMenuState); - pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); - pw.println(innerPrefix + "mImeHeight=" + mImeHeight); - pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); - pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); - pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); - pw.println(innerPrefix + "mEnableDragToEdgeDismiss=" + mEnableDismissDragToEdge); - pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets); - mPipBoundsHandler.dump(pw, innerPrefix); - mTouchState.dump(pw, innerPrefix); - mMotionHelper.dump(pw, innerPrefix); - if (mPipResizeGestureHandler != null) { - mPipResizeGestureHandler.dump(pw, innerPrefix); - } - } - -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java deleted file mode 100644 index ecd1128a5680..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchState.java +++ /dev/null @@ -1,389 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.phone; - -import android.graphics.PointF; -import android.os.Handler; -import android.util.Log; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.ViewConfiguration; - -import com.android.internal.annotations.VisibleForTesting; - -import java.io.PrintWriter; - -/** - * This keeps track of the touch state throughout the current touch gesture. - */ -public class PipTouchState { - private static final String TAG = "PipTouchState"; - private static final boolean DEBUG = false; - - @VisibleForTesting - static final long DOUBLE_TAP_TIMEOUT = 200; - static final long HOVER_EXIT_TIMEOUT = 50; - - private final Handler mHandler; - private final ViewConfiguration mViewConfig; - private final Runnable mDoubleTapTimeoutCallback; - private final Runnable mHoverExitTimeoutCallback; - - private VelocityTracker mVelocityTracker; - private long mDownTouchTime = 0; - private long mLastDownTouchTime = 0; - private long mUpTouchTime = 0; - private final PointF mDownTouch = new PointF(); - private final PointF mDownDelta = new PointF(); - private final PointF mLastTouch = new PointF(); - private final PointF mLastDelta = new PointF(); - private final PointF mVelocity = new PointF(); - private boolean mAllowTouches = true; - private boolean mIsUserInteracting = false; - // Set to true only if the multiple taps occur within the double tap timeout - private boolean mIsDoubleTap = false; - // Set to true only if a gesture - private boolean mIsWaitingForDoubleTap = false; - private boolean mIsDragging = false; - // The previous gesture was a drag - private boolean mPreviouslyDragging = false; - private boolean mStartedDragging = false; - private boolean mAllowDraggingOffscreen = false; - private int mActivePointerId; - - public PipTouchState(ViewConfiguration viewConfig, Handler handler, - Runnable doubleTapTimeoutCallback, Runnable hoverExitTimeoutCallback) { - mViewConfig = viewConfig; - mHandler = handler; - mDoubleTapTimeoutCallback = doubleTapTimeoutCallback; - mHoverExitTimeoutCallback = hoverExitTimeoutCallback; - } - - /** - * Resets this state. - */ - public void reset() { - mAllowDraggingOffscreen = false; - mIsDragging = false; - mStartedDragging = false; - mIsUserInteracting = false; - } - - /** - * Processes a given touch event and updates the state. - */ - public void onTouchEvent(MotionEvent ev) { - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - if (!mAllowTouches) { - return; - } - - // Initialize the velocity tracker - initOrResetVelocityTracker(); - addMovementToVelocityTracker(ev); - - mActivePointerId = ev.getPointerId(0); - if (DEBUG) { - Log.e(TAG, "Setting active pointer id on DOWN: " + mActivePointerId); - } - mLastTouch.set(ev.getRawX(), ev.getRawY()); - mDownTouch.set(mLastTouch); - mAllowDraggingOffscreen = true; - mIsUserInteracting = true; - mDownTouchTime = ev.getEventTime(); - mIsDoubleTap = !mPreviouslyDragging && - (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT; - mIsWaitingForDoubleTap = false; - mIsDragging = false; - mLastDownTouchTime = mDownTouchTime; - if (mDoubleTapTimeoutCallback != null) { - mHandler.removeCallbacks(mDoubleTapTimeoutCallback); - } - break; - } - case MotionEvent.ACTION_MOVE: { - // Skip event if we did not start processing this touch gesture - if (!mIsUserInteracting) { - break; - } - - // Update the velocity tracker - addMovementToVelocityTracker(ev); - int pointerIndex = ev.findPointerIndex(mActivePointerId); - if (pointerIndex == -1) { - Log.e(TAG, "Invalid active pointer id on MOVE: " + mActivePointerId); - break; - } - - float x = ev.getRawX(pointerIndex); - float y = ev.getRawY(pointerIndex); - mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y); - mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y); - - boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop(); - if (!mIsDragging) { - if (hasMovedBeyondTap) { - mIsDragging = true; - mStartedDragging = true; - } - } else { - mStartedDragging = false; - } - mLastTouch.set(x, y); - break; - } - case MotionEvent.ACTION_POINTER_UP: { - // Skip event if we did not start processing this touch gesture - if (!mIsUserInteracting) { - break; - } - - // Update the velocity tracker - addMovementToVelocityTracker(ev); - - int pointerIndex = ev.getActionIndex(); - int pointerId = ev.getPointerId(pointerIndex); - if (pointerId == mActivePointerId) { - // Select a new active pointer id and reset the movement state - final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; - mActivePointerId = ev.getPointerId(newPointerIndex); - if (DEBUG) { - Log.e(TAG, "Relinquish active pointer id on POINTER_UP: " + - mActivePointerId); - } - mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex)); - } - break; - } - case MotionEvent.ACTION_UP: { - // Skip event if we did not start processing this touch gesture - if (!mIsUserInteracting) { - break; - } - - // Update the velocity tracker - addMovementToVelocityTracker(ev); - mVelocityTracker.computeCurrentVelocity(1000, - mViewConfig.getScaledMaximumFlingVelocity()); - mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); - - int pointerIndex = ev.findPointerIndex(mActivePointerId); - if (pointerIndex == -1) { - Log.e(TAG, "Invalid active pointer id on UP: " + mActivePointerId); - break; - } - - mUpTouchTime = ev.getEventTime(); - mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex)); - mPreviouslyDragging = mIsDragging; - mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging && - (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT; - - // Fall through to clean up - } - case MotionEvent.ACTION_CANCEL: { - recycleVelocityTracker(); - break; - } - case MotionEvent.ACTION_BUTTON_PRESS: { - removeHoverExitTimeoutCallback(); - break; - } - } - } - - /** - * @return the velocity of the active touch pointer at the point it is lifted off the screen. - */ - public PointF getVelocity() { - return mVelocity; - } - - /** - * @return the last touch position of the active pointer. - */ - public PointF getLastTouchPosition() { - return mLastTouch; - } - - /** - * @return the movement delta between the last handled touch event and the previous touch - * position. - */ - public PointF getLastTouchDelta() { - return mLastDelta; - } - - /** - * @return the down touch position. - */ - public PointF getDownTouchPosition() { - return mDownTouch; - } - - /** - * @return the movement delta between the last handled touch event and the down touch - * position. - */ - public PointF getDownTouchDelta() { - return mDownDelta; - } - - /** - * @return whether the user has started dragging. - */ - public boolean isDragging() { - return mIsDragging; - } - - /** - * @return whether the user is currently interacting with the PiP. - */ - public boolean isUserInteracting() { - return mIsUserInteracting; - } - - /** - * @return whether the user has started dragging just in the last handled touch event. - */ - public boolean startedDragging() { - return mStartedDragging; - } - - /** - * Sets whether touching is currently allowed. - */ - public void setAllowTouches(boolean allowTouches) { - mAllowTouches = allowTouches; - - // If the user happens to touch down before this is sent from the system during a transition - // then block any additional handling by resetting the state now - if (mIsUserInteracting) { - reset(); - } - } - - /** - * Disallows dragging offscreen for the duration of the current gesture. - */ - public void setDisallowDraggingOffscreen() { - mAllowDraggingOffscreen = false; - } - - /** - * @return whether dragging offscreen is allowed during this gesture. - */ - public boolean allowDraggingOffscreen() { - return mAllowDraggingOffscreen; - } - - /** - * @return whether this gesture is a double-tap. - */ - public boolean isDoubleTap() { - return mIsDoubleTap; - } - - /** - * @return whether this gesture will potentially lead to a following double-tap. - */ - public boolean isWaitingForDoubleTap() { - return mIsWaitingForDoubleTap; - } - - /** - * Schedules the callback to run if the next double tap does not occur. Only runs if - * isWaitingForDoubleTap() is true. - */ - public void scheduleDoubleTapTimeoutCallback() { - if (mIsWaitingForDoubleTap) { - long delay = getDoubleTapTimeoutCallbackDelay(); - mHandler.removeCallbacks(mDoubleTapTimeoutCallback); - mHandler.postDelayed(mDoubleTapTimeoutCallback, delay); - } - } - - @VisibleForTesting long getDoubleTapTimeoutCallbackDelay() { - if (mIsWaitingForDoubleTap) { - return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime)); - } - return -1; - } - - /** - * Removes the timeout callback if it's in queue. - */ - public void removeDoubleTapTimeoutCallback() { - mIsWaitingForDoubleTap = false; - mHandler.removeCallbacks(mDoubleTapTimeoutCallback); - } - - void scheduleHoverExitTimeoutCallback() { - mHandler.removeCallbacks(mHoverExitTimeoutCallback); - mHandler.postDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT); - } - - void removeHoverExitTimeoutCallback() { - mHandler.removeCallbacks(mHoverExitTimeoutCallback); - } - - void addMovementToVelocityTracker(MotionEvent event) { - if (mVelocityTracker == null) { - return; - } - - // Add movement to velocity tracker using raw screen X and Y coordinates instead - // of window coordinates because the window frame may be moving at the same time. - float deltaX = event.getRawX() - event.getX(); - float deltaY = event.getRawY() - event.getY(); - event.offsetLocation(deltaX, deltaY); - mVelocityTracker.addMovement(event); - event.offsetLocation(-deltaX, -deltaY); - } - - private void initOrResetVelocityTracker() { - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } else { - mVelocityTracker.clear(); - } - } - - private void recycleVelocityTracker() { - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - } - - public void dump(PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.println(prefix + TAG); - pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches); - pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId); - pw.println(innerPrefix + "mDownTouch=" + mDownTouch); - pw.println(innerPrefix + "mDownDelta=" + mDownDelta); - pw.println(innerPrefix + "mLastTouch=" + mLastTouch); - pw.println(innerPrefix + "mLastDelta=" + mLastDelta); - pw.println(innerPrefix + "mVelocity=" + mVelocity); - pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting); - pw.println(innerPrefix + "mIsDragging=" + mIsDragging); - pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging); - pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipUpdateThread.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipUpdateThread.java deleted file mode 100644 index 6c5d84645e92..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipUpdateThread.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2015 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.pip.phone; - -import android.os.Handler; -import android.os.HandlerThread; - -/** - * Similar to {@link com.android.internal.os.BackgroundThread}, this is a shared singleton - * foreground thread for each process for updating PIP. - */ -public final class PipUpdateThread extends HandlerThread { - private static PipUpdateThread sInstance; - private static Handler sHandler; - - private PipUpdateThread() { - super("pip"); - } - - private static void ensureThreadLocked() { - if (sInstance == null) { - sInstance = new PipUpdateThread(); - sInstance.start(); - sHandler = new Handler(sInstance.getLooper()); - } - } - - /** - * @return the static update thread instance - */ - public static PipUpdateThread get() { - synchronized (PipUpdateThread.class) { - ensureThreadLocked(); - return sInstance; - } - } - /** - * @return the static update thread handler instance - */ - public static Handler getHandler() { - synchronized (PipUpdateThread.class) { - ensureThreadLocked(); - return sHandler; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipUtils.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipUtils.java deleted file mode 100644 index baa8f118f362..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipUtils.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2017 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.pip.phone; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; - -import android.app.ActivityTaskManager; -import android.app.ActivityTaskManager.RootTaskInfo; -import android.app.IActivityManager; -import android.content.ComponentName; -import android.content.Context; -import android.os.RemoteException; -import android.util.Log; -import android.util.Pair; - -public class PipUtils { - - private static final String TAG = "PipUtils"; - - /** - * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack. - * The component name may be null if no such activity exists. - */ - public static Pair<ComponentName, Integer> getTopPipActivity(Context context, - IActivityManager activityManager) { - try { - final String sysUiPackageName = context.getPackageName(); - final RootTaskInfo pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo( - WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); - if (pinnedTaskInfo != null && pinnedTaskInfo.childTaskIds != null - && pinnedTaskInfo.childTaskIds.length > 0) { - for (int i = pinnedTaskInfo.childTaskNames.length - 1; i >= 0; i--) { - ComponentName cn = ComponentName.unflattenFromString( - pinnedTaskInfo.childTaskNames[i]); - if (cn != null && !cn.getPackageName().equals(sysUiPackageName)) { - return new Pair<>(cn, pinnedTaskInfo.childTaskUserIds[i]); - } - } - } - } catch (RemoteException e) { - Log.w(TAG, "Unable to get pinned stack."); - } - return new Pair<>(null, 0); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlButtonView.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlButtonView.java deleted file mode 100644 index db9bedd2e620..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlButtonView.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.tv; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import com.android.wm.shell.R; - -/** - * A view containing PIP controls including fullscreen, close, and media controls. - */ -public class PipControlButtonView extends RelativeLayout { - - private OnFocusChangeListener mFocusChangeListener; - private ImageView mIconImageView; - ImageView mButtonImageView; - private TextView mDescriptionTextView; - private Animator mTextFocusGainAnimator; - private Animator mButtonFocusGainAnimator; - private Animator mTextFocusLossAnimator; - private Animator mButtonFocusLossAnimator; - - private final OnFocusChangeListener mInternalFocusChangeListener = - new OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - startFocusGainAnimation(); - } else { - startFocusLossAnimation(); - } - - if (mFocusChangeListener != null) { - mFocusChangeListener.onFocusChange(PipControlButtonView.this, hasFocus); - } - } - }; - - public PipControlButtonView(Context context) { - this(context, null, 0, 0); - } - - public PipControlButtonView(Context context, AttributeSet attrs) { - this(context, attrs, 0, 0); - } - - public PipControlButtonView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public PipControlButtonView( - Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - LayoutInflater inflater = (LayoutInflater) getContext() - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.tv_pip_control_button, this); - - mIconImageView = findViewById(R.id.icon); - mButtonImageView = findViewById(R.id.button); - mDescriptionTextView = findViewById(R.id.desc); - - int[] values = new int[] {android.R.attr.src, android.R.attr.text}; - TypedArray typedArray = - context.obtainStyledAttributes(attrs, values, defStyleAttr, defStyleRes); - - setImageResource(typedArray.getResourceId(0, 0)); - setText(typedArray.getResourceId(1, 0)); - - typedArray.recycle(); - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - mButtonImageView.setOnFocusChangeListener(mInternalFocusChangeListener); - - mTextFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), - R.anim.tv_pip_controls_focus_gain_animation); - mTextFocusGainAnimator.setTarget(mDescriptionTextView); - mButtonFocusGainAnimator = AnimatorInflater.loadAnimator(getContext(), - R.anim.tv_pip_controls_focus_gain_animation); - mButtonFocusGainAnimator.setTarget(mButtonImageView); - - mTextFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(), - R.anim.tv_pip_controls_focus_loss_animation); - mTextFocusLossAnimator.setTarget(mDescriptionTextView); - mButtonFocusLossAnimator = AnimatorInflater.loadAnimator(getContext(), - R.anim.tv_pip_controls_focus_loss_animation); - mButtonFocusLossAnimator.setTarget(mButtonImageView); - } - - @Override - public void setOnClickListener(OnClickListener listener) { - mButtonImageView.setOnClickListener(listener); - } - - @Override - public void setOnFocusChangeListener(OnFocusChangeListener listener) { - mFocusChangeListener = listener; - } - - /** - * Sets the drawable for the button with the given drawable. - */ - public void setImageDrawable(Drawable d) { - mIconImageView.setImageDrawable(d); - } - - /** - * Sets the drawable for the button with the given resource id. - */ - public void setImageResource(int resId) { - if (resId != 0) { - mIconImageView.setImageResource(resId); - } - } - - /** - * Sets the text for description the with the given string. - */ - public void setText(CharSequence text) { - mButtonImageView.setContentDescription(text); - mDescriptionTextView.setText(text); - } - - /** - * Sets the text for description the with the given resource id. - */ - public void setText(int resId) { - if (resId != 0) { - mButtonImageView.setContentDescription(getContext().getString(resId)); - mDescriptionTextView.setText(resId); - } - } - - private static void cancelAnimator(Animator animator) { - if (animator.isStarted()) { - animator.cancel(); - } - } - - /** - * Starts the focus gain animation. - */ - public void startFocusGainAnimation() { - cancelAnimator(mButtonFocusLossAnimator); - cancelAnimator(mTextFocusLossAnimator); - mTextFocusGainAnimator.start(); - if (mButtonImageView.getAlpha() < 1f) { - // If we had faded out the ripple drawable, run our manual focus change animation. - // See the comment at {@link #startFocusLossAnimation()} for the reason of manual - // animator. - mButtonFocusGainAnimator.start(); - } - } - - /** - * Starts the focus loss animation. - */ - public void startFocusLossAnimation() { - cancelAnimator(mButtonFocusGainAnimator); - cancelAnimator(mTextFocusGainAnimator); - mTextFocusLossAnimator.start(); - if (mButtonImageView.hasFocus()) { - // Button uses ripple that has the default animation for the focus changes. - // Howevever, it doesn't expose the API to fade out while it is focused, - // so we should manually run the fade out animation when PIP controls row loses focus. - mButtonFocusLossAnimator.start(); - } - } - - /** - * Resets to initial state. - */ - public void reset() { - cancelAnimator(mButtonFocusGainAnimator); - cancelAnimator(mTextFocusGainAnimator); - cancelAnimator(mButtonFocusLossAnimator); - cancelAnimator(mTextFocusLossAnimator); - mButtonImageView.setAlpha(1f); - mDescriptionTextView.setAlpha(mButtonImageView.hasFocus() ? 1f : 0f); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipController.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipController.java deleted file mode 100644 index 12a545aa4b02..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipController.java +++ /dev/null @@ -1,762 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.tv; - -import static android.app.ActivityTaskManager.INVALID_STACK_ID; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; - -import android.app.ActivityManager.RunningTaskInfo; -import android.app.ActivityTaskManager; -import android.app.ActivityTaskManager.RootTaskInfo; -import android.app.IActivityTaskManager; -import android.app.RemoteAction; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ParceledListSlice; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Rect; -import android.media.session.MediaController; -import android.media.session.MediaSessionManager; -import android.media.session.PlaybackState; -import android.os.Debug; -import android.os.Handler; -import android.os.RemoteException; -import android.os.UserHandle; -import android.text.TextUtils; -import android.util.Log; -import android.view.DisplayInfo; - -import com.android.systemui.Dependency; -import com.android.systemui.R; -import com.android.systemui.UiOffloadThread; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.pip.Pip; -import com.android.systemui.pip.PipBoundsHandler; -import com.android.systemui.pip.PipSurfaceTransactionHelper; -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.PinnedStackListenerForwarder.PinnedStackListener; -import com.android.systemui.shared.system.TaskStackChangeListener; -import com.android.systemui.shared.system.WindowManagerWrapper; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/** - * Manages the picture-in-picture (PIP) UI and states. - */ -@SysUISingleton -public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallback { - private static final String TAG = "PipController"; - static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - /** - * Unknown or invalid state - */ - public static final int STATE_UNKNOWN = -1; - /** - * State when there's no PIP. - */ - public static final int STATE_NO_PIP = 0; - /** - * State when PIP is shown. This is used as default PIP state. - */ - public static final int STATE_PIP = 1; - /** - * State when PIP menu dialog is shown. - */ - public static final int STATE_PIP_MENU = 2; - - private static final int TASK_ID_NO_PIP = -1; - private static final int INVALID_RESOURCE_TYPE = -1; - - public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH = 0x1; - - /** - * PIPed activity is playing a media and it can be paused. - */ - static final int PLAYBACK_STATE_PLAYING = 0; - /** - * PIPed activity has a paused media and it can be played. - */ - static final int PLAYBACK_STATE_PAUSED = 1; - /** - * Users are unable to control PIPed activity's media playback. - */ - static final int PLAYBACK_STATE_UNAVAILABLE = 2; - - private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000; - - private int mSuspendPipResizingReason; - - private Context mContext; - private PipBoundsHandler mPipBoundsHandler; - private PipTaskOrganizer mPipTaskOrganizer; - private PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; - private IActivityTaskManager mActivityTaskManager; - private MediaSessionManager mMediaSessionManager; - private int mState = STATE_NO_PIP; - private int mResumeResizePinnedStackRunnableState = STATE_NO_PIP; - private final Handler mHandler = new Handler(); - private List<Listener> mListeners = new ArrayList<>(); - private List<MediaListener> mMediaListeners = new ArrayList<>(); - private Rect mPipBounds; - private Rect mDefaultPipBounds = new Rect(); - private Rect mMenuModePipBounds; - private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED; - private boolean mInitialized; - private int mPipTaskId = TASK_ID_NO_PIP; - private int mPinnedStackId = INVALID_STACK_ID; - private ComponentName mPipComponentName; - private MediaController mPipMediaController; - private String[] mLastPackagesResourceGranted; - private PipNotification mPipNotification; - private ParceledListSlice<RemoteAction> mCustomActions; - private int mResizeAnimationDuration; - - // Used to calculate the movement bounds - private final DisplayInfo mTmpDisplayInfo = new DisplayInfo(); - private final Rect mTmpInsetBounds = new Rect(); - - // Keeps track of the IME visibility to adjust the PiP when the IME is visible - private boolean mImeVisible; - private int mImeHeightAdjustment; - - private final PinnedStackListener mPinnedStackListener = new PipControllerPinnedStackListener(); - - private final Runnable mResizePinnedStackRunnable = new Runnable() { - @Override - public void run() { - resizePinnedStack(mResumeResizePinnedStackRunnableState); - } - }; - private final Runnable mClosePipRunnable = new Runnable() { - @Override - public void run() { - closePip(); - } - }; - - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_MEDIA_RESOURCE_GRANTED.equals(action)) { - String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); - int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE, - INVALID_RESOURCE_TYPE); - if (packageNames != null && packageNames.length > 0 - && resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) { - handleMediaResourceGranted(packageNames); - } - } - - } - }; - private final MediaSessionManager.OnActiveSessionsChangedListener mActiveMediaSessionListener = - new MediaSessionManager.OnActiveSessionsChangedListener() { - @Override - public void onActiveSessionsChanged(List<MediaController> controllers) { - updateMediaController(controllers); - } - }; - - /** - * Handler for messages from the PIP controller. - */ - private class PipControllerPinnedStackListener extends PinnedStackListener { - @Override - public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { - mHandler.post(() -> { - mPipBoundsHandler.onImeVisibilityChanged(imeVisible, imeHeight); - if (mState == STATE_PIP) { - if (mImeVisible != imeVisible) { - if (imeVisible) { - // Save the IME height adjustment, and offset to not occlude the IME - mPipBounds.offset(0, -imeHeight); - mImeHeightAdjustment = imeHeight; - } else { - // Apply the inverse adjustment when the IME is hidden - mPipBounds.offset(0, mImeHeightAdjustment); - } - mImeVisible = imeVisible; - resizePinnedStack(STATE_PIP); - } - } - }); - } - - @Override - public void onMovementBoundsChanged(boolean fromImeAdjustment) { - mHandler.post(() -> { - // Populate the inset / normal bounds and DisplayInfo from mPipBoundsHandler first. - mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mPipBounds, - mDefaultPipBounds, mTmpDisplayInfo); - }); - } - - @Override - public void onActionsChanged(ParceledListSlice<RemoteAction> actions) { - mCustomActions = actions; - mHandler.post(() -> { - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onPipMenuActionsChanged(mCustomActions); - } - }); - } - } - - public PipController(Context context, BroadcastDispatcher broadcastDispatcher, - PipBoundsHandler pipBoundsHandler, - PipSurfaceTransactionHelper pipSurfaceTransactionHelper, - PipTaskOrganizer pipTaskOrganizer) { - if (mInitialized) { - return; - } - - mInitialized = true; - mContext = context; - mPipNotification = new PipNotification(context, broadcastDispatcher, - Optional.of(this).get()); - mPipBoundsHandler = pipBoundsHandler; - // Ensure that we have the display info in case we get calls to update the bounds before the - // listener calls back - final DisplayInfo displayInfo = new DisplayInfo(); - context.getDisplay().getDisplayInfo(displayInfo); - mPipBoundsHandler.onDisplayInfoChanged(displayInfo); - - mResizeAnimationDuration = context.getResources() - .getInteger(R.integer.config_pipResizeAnimationDuration); - mPipSurfaceTransactionHelper = pipSurfaceTransactionHelper; - mPipTaskOrganizer = pipTaskOrganizer; - mPipTaskOrganizer.registerPipTransitionCallback(this); - mActivityTaskManager = ActivityTaskManager.getService(); - ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_MEDIA_RESOURCE_GRANTED); - broadcastDispatcher.registerReceiver(mBroadcastReceiver, intentFilter, - null /* handler */, UserHandle.ALL); - - // Initialize the last orientation and apply the current configuration - Configuration initialConfig = mContext.getResources().getConfiguration(); - mLastOrientation = initialConfig.orientation; - loadConfigurationsAndApply(initialConfig); - - mMediaSessionManager = - (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE); - - try { - WindowManagerWrapper.getInstance().addPinnedStackListener(mPinnedStackListener); - } catch (RemoteException | UnsupportedOperationException e) { - Log.e(TAG, "Failed to register pinned stack listener", e); - } - } - - private void loadConfigurationsAndApply(Configuration newConfig) { - if (mLastOrientation != newConfig.orientation) { - // Don't resize the pinned stack on orientation change. TV does not care about this case - // and this could clobber the existing animation to the new bounds calculated by WM. - mLastOrientation = newConfig.orientation; - return; - } - - Resources res = mContext.getResources(); - mMenuModePipBounds = Rect.unflattenFromString(res.getString( - R.string.pip_menu_bounds)); - - // Reset the PIP bounds and apply. PIP bounds can be changed by two reasons. - // 1. Configuration changed due to the language change (RTL <-> RTL) - // 2. SystemUI restarts after the crash - mPipBounds = mDefaultPipBounds; - resizePinnedStack(getPinnedTaskInfo() == null ? STATE_NO_PIP : STATE_PIP); - } - - /** - * Updates the PIP per configuration changed. - */ - public void onConfigurationChanged(Configuration newConfig) { - loadConfigurationsAndApply(newConfig); - mPipNotification.onConfigurationChanged(mContext); - } - - /** - * Shows the picture-in-picture menu if an activity is in picture-in-picture mode. - */ - public void showPictureInPictureMenu() { - if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), current state=" + getStateDescription()); - - if (getState() == STATE_PIP) { - resizePinnedStack(STATE_PIP_MENU); - } - } - - /** - * Closes PIP (PIPed activity and PIP system UI). - */ - public void closePip() { - if (DEBUG) Log.d(TAG, "closePip(), current state=" + getStateDescription()); - - closePipInternal(true); - } - - private void closePipInternal(boolean removePipStack) { - if (DEBUG) { - Log.d(TAG, - "closePipInternal() removePipStack=" + removePipStack + ", current state=" - + getStateDescription()); - } - - mState = STATE_NO_PIP; - mPipTaskId = TASK_ID_NO_PIP; - mPipMediaController = null; - mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener); - if (removePipStack) { - try { - mActivityTaskManager.removeStack(mPinnedStackId); - } catch (RemoteException e) { - Log.e(TAG, "removeStack failed", e); - } finally { - mPinnedStackId = INVALID_STACK_ID; - } - } - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onPipActivityClosed(); - } - mHandler.removeCallbacks(mClosePipRunnable); - updatePipVisibility(false); - } - - /** - * Moves the PIPed activity to the fullscreen and closes PIP system UI. - */ - public void movePipToFullscreen() { - if (DEBUG) Log.d(TAG, "movePipToFullscreen(), current state=" + getStateDescription()); - - mPipTaskId = TASK_ID_NO_PIP; - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onMoveToFullscreen(); - } - resizePinnedStack(STATE_NO_PIP); - updatePipVisibility(false); - } - - /** - * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called - * - * @param reason The reason for suspending resizing operations on the Pip. - */ - public void suspendPipResizing(int reason) { - if (DEBUG) { - Log.d(TAG, - "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); - } - - mSuspendPipResizingReason |= reason; - } - - /** - * Resumes resizing operation on the Pip that was previously suspended. - * - * @param reason The reason resizing operations on the Pip was suspended. - */ - public void resumePipResizing(int reason) { - if ((mSuspendPipResizingReason & reason) == 0) { - return; - } - if (DEBUG) { - Log.d(TAG, - "resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2)); - } - mSuspendPipResizingReason &= ~reason; - mHandler.post(mResizePinnedStackRunnable); - } - - /** - * Resize the Pip to the appropriate size for the input state. - * - * @param state In Pip state also used to determine the new size for the Pip. - */ - public void resizePinnedStack(int state) { - if (DEBUG) { - Log.d(TAG, "resizePinnedStack() state=" + stateToName(state) + ", current state=" - + getStateDescription(), new Exception()); - } - - boolean wasStateNoPip = (mState == STATE_NO_PIP); - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onPipResizeAboutToStart(); - } - if (mSuspendPipResizingReason != 0) { - mResumeResizePinnedStackRunnableState = state; - if (DEBUG) { - Log.d(TAG, "resizePinnedStack() deferring" - + " mSuspendPipResizingReason=" + mSuspendPipResizingReason - + " mResumeResizePinnedStackRunnableState=" - + stateToName(mResumeResizePinnedStackRunnableState)); - } - return; - } - mState = state; - final Rect newBounds; - switch (mState) { - case STATE_NO_PIP: - newBounds = null; - // If the state was already STATE_NO_PIP, then do not resize the stack below as it - // will not exist - if (wasStateNoPip) { - return; - } - break; - case STATE_PIP_MENU: - newBounds = mMenuModePipBounds; - break; - case STATE_PIP: // fallthrough - default: - newBounds = mPipBounds; - break; - } - if (newBounds != null) { - mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null); - } else { - mPipTaskOrganizer.exitPip(mResizeAnimationDuration); - } - } - - /** - * @return the current state, or the pending state if the state change was previously suspended. - */ - private int getState() { - if (mSuspendPipResizingReason != 0) { - return mResumeResizePinnedStackRunnableState; - } - return mState; - } - - /** - * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned - * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}. - */ - private void showPipMenu() { - if (DEBUG) Log.d(TAG, "showPipMenu(), current state=" + getStateDescription()); - - mState = STATE_PIP_MENU; - for (int i = mListeners.size() - 1; i >= 0; --i) { - mListeners.get(i).onShowPipMenu(); - } - Intent intent = new Intent(mContext, PipMenuActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(PipMenuActivity.EXTRA_CUSTOM_ACTIONS, mCustomActions); - mContext.startActivity(intent); - } - - /** - * Adds a {@link Listener} to PipController. - */ - public void addListener(Listener listener) { - mListeners.add(listener); - } - - /** - * Removes a {@link Listener} from PipController. - */ - public void removeListener(Listener listener) { - mListeners.remove(listener); - } - - /** - * Adds a {@link MediaListener} to PipController. - */ - public void addMediaListener(MediaListener listener) { - mMediaListeners.add(listener); - } - - /** - * Removes a {@link MediaListener} from PipController. - */ - public void removeMediaListener(MediaListener listener) { - mMediaListeners.remove(listener); - } - - /** - * Returns {@code true} if PIP is shown. - */ - public boolean isPipShown() { - return mState != STATE_NO_PIP; - } - - private RootTaskInfo getPinnedTaskInfo() { - RootTaskInfo taskInfo = null; - try { - taskInfo = ActivityTaskManager.getService().getRootTaskInfo( - WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); - } catch (RemoteException e) { - Log.e(TAG, "getRootTaskInfo failed", e); - } - return taskInfo; - } - - private void handleMediaResourceGranted(String[] packageNames) { - if (getState() == STATE_NO_PIP) { - mLastPackagesResourceGranted = packageNames; - } else { - boolean requestedFromLastPackages = false; - if (mLastPackagesResourceGranted != null) { - for (String packageName : mLastPackagesResourceGranted) { - for (String newPackageName : packageNames) { - if (TextUtils.equals(newPackageName, packageName)) { - requestedFromLastPackages = true; - break; - } - } - } - } - mLastPackagesResourceGranted = packageNames; - if (!requestedFromLastPackages) { - closePip(); - } - } - } - - private void updateMediaController(List<MediaController> controllers) { - MediaController mediaController = null; - if (controllers != null && getState() != STATE_NO_PIP && mPipComponentName != null) { - for (int i = controllers.size() - 1; i >= 0; i--) { - MediaController controller = controllers.get(i); - // We assumes that an app with PIPable activity - // keeps the single instance of media controller especially when PIP is on. - if (controller.getPackageName().equals(mPipComponentName.getPackageName())) { - mediaController = controller; - break; - } - } - } - if (mPipMediaController != mediaController) { - mPipMediaController = mediaController; - for (int i = mMediaListeners.size() - 1; i >= 0; i--) { - mMediaListeners.get(i).onMediaControllerChanged(); - } - if (mPipMediaController == null) { - mHandler.postDelayed(mClosePipRunnable, - CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS); - } else { - mHandler.removeCallbacks(mClosePipRunnable); - } - } - } - - /** - * Gets the {@link android.media.session.MediaController} for the PIPed activity. - */ - public MediaController getMediaController() { - return mPipMediaController; - } - - @Override - public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) { - - } - - /** - * Returns the PIPed activity's playback state. - * This returns one of {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, - * or {@link #PLAYBACK_STATE_UNAVAILABLE}. - */ - public int getPlaybackState() { - if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) { - return PLAYBACK_STATE_UNAVAILABLE; - } - int state = mPipMediaController.getPlaybackState().getState(); - boolean isPlaying = (state == PlaybackState.STATE_BUFFERING - || state == PlaybackState.STATE_CONNECTING - || state == PlaybackState.STATE_PLAYING - || state == PlaybackState.STATE_FAST_FORWARDING - || state == PlaybackState.STATE_REWINDING - || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS - || state == PlaybackState.STATE_SKIPPING_TO_NEXT); - long actions = mPipMediaController.getPlaybackState().getActions(); - if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) { - return PLAYBACK_STATE_PAUSED; - } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) { - return PLAYBACK_STATE_PLAYING; - } - return PLAYBACK_STATE_UNAVAILABLE; - } - - private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { - @Override - public void onTaskStackChanged() { - if (DEBUG) Log.d(TAG, "onTaskStackChanged()"); - - if (getState() != STATE_NO_PIP) { - boolean hasPip = false; - - RootTaskInfo taskInfo = getPinnedTaskInfo(); - if (taskInfo == null || taskInfo.childTaskIds == null) { - Log.w(TAG, "There is nothing in pinned stack"); - closePipInternal(false); - return; - } - for (int i = taskInfo.childTaskIds.length - 1; i >= 0; --i) { - if (taskInfo.childTaskIds[i] == mPipTaskId) { - // PIP task is still alive. - hasPip = true; - break; - } - } - if (!hasPip) { - // PIP task doesn't exist anymore in PINNED_STACK. - closePipInternal(true); - return; - } - } - if (getState() == STATE_PIP) { - if (mPipBounds != mDefaultPipBounds) { - mPipBounds = mDefaultPipBounds; - resizePinnedStack(STATE_PIP); - } - } - } - - @Override - public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { - if (DEBUG) Log.d(TAG, "onActivityPinned()"); - - RootTaskInfo taskInfo = getPinnedTaskInfo(); - if (taskInfo == null) { - Log.w(TAG, "Cannot find pinned stack"); - return; - } - if (DEBUG) Log.d(TAG, "PINNED_STACK:" + taskInfo); - mPinnedStackId = taskInfo.taskId; - mPipTaskId = taskInfo.childTaskIds[taskInfo.childTaskIds.length - 1]; - mPipComponentName = ComponentName.unflattenFromString( - taskInfo.childTaskNames[taskInfo.childTaskNames.length - 1]); - // Set state to STATE_PIP so we show it when the pinned stack animation ends. - mState = STATE_PIP; - mMediaSessionManager.addOnActiveSessionsChangedListener( - mActiveMediaSessionListener, null); - updateMediaController(mMediaSessionManager.getActiveSessions(null)); - for (int i = mListeners.size() - 1; i >= 0; i--) { - mListeners.get(i).onPipEntered(packageName); - } - updatePipVisibility(true); - } - - @Override - public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, - boolean clearedTask, boolean wasVisible) { - if (task.configuration.windowConfiguration.getWindowingMode() - != WINDOWING_MODE_PINNED) { - return; - } - if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()"); - - // If PIPed activity is launched again by Launcher or intent, make it fullscreen. - movePipToFullscreen(); - } - }; - - @Override - public void onPipTransitionStarted(ComponentName activity, int direction, Rect pipBounds) { - } - - @Override - public void onPipTransitionFinished(ComponentName activity, int direction) { - onPipTransitionFinishedOrCanceled(); - } - - @Override - public void onPipTransitionCanceled(ComponentName activity, int direction) { - onPipTransitionFinishedOrCanceled(); - } - - private void onPipTransitionFinishedOrCanceled() { - if (DEBUG) Log.d(TAG, "onPipTransitionFinishedOrCanceled()"); - - if (getState() == STATE_PIP_MENU) { - showPipMenu(); - } - } - - /** - * A listener interface to receive notification on changes in PIP. - */ - public interface Listener { - /** - * Invoked when an activity is pinned and PIP manager is set corresponding information. - * Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned} - * because there's no guarantee for the PIP manager be return relavent information - * correctly. (e.g. {@link Pip.isPipShown}). - */ - void onPipEntered(String packageName); - /** Invoked when a PIPed activity is closed. */ - void onPipActivityClosed(); - /** Invoked when the PIP menu gets shown. */ - void onShowPipMenu(); - /** Invoked when the PIP menu actions change. */ - void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions); - /** Invoked when the PIPed activity is about to return back to the fullscreen. */ - void onMoveToFullscreen(); - /** Invoked when we are above to start resizing the Pip. */ - void onPipResizeAboutToStart(); - } - - /** - * A listener interface to receive change in PIP's media controller - */ - public interface MediaListener { - /** Invoked when the MediaController on PIPed activity is changed. */ - void onMediaControllerChanged(); - } - - private void updatePipVisibility(final boolean visible) { - Dependency.get(UiOffloadThread.class).execute(() -> { - WindowManagerWrapper.getInstance().setPipVisibility(visible); - }); - } - - private String getStateDescription() { - if (mSuspendPipResizingReason == 0) { - return stateToName(mState); - } - return stateToName(mResumeResizePinnedStackRunnableState) + " (while " + stateToName(mState) - + " is suspended)"; - } - - private static String stateToName(int state) { - switch (state) { - case STATE_NO_PIP: - return "NO_PIP"; - - case STATE_PIP: - return "PIP"; - - case STATE_PIP_MENU: - return "PIP_MENU"; - - default: - return "UNKNOWN(" + state + ")"; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlsView.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlsView.java deleted file mode 100644 index 125444d2dfb5..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlsView.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.tv; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.widget.LinearLayout; - -import com.android.wm.shell.R; - - -/** - * A view containing PIP controls including fullscreen, close, and media controls. - */ -public class PipControlsView extends LinearLayout { - - public PipControlsView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public PipControlsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - layoutInflater.inflate(R.layout.tv_pip_controls, this); - setOrientation(LinearLayout.HORIZONTAL); - setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); - } - - PipControlButtonView getFullButtonView() { - return findViewById(R.id.full_button); - } - - PipControlButtonView getCloseButtonView() { - return findViewById(R.id.close_button); - } - - PipControlButtonView getPlayPauseButtonView() { - return findViewById(R.id.play_pause_button); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlsViewController.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlsViewController.java deleted file mode 100644 index 4ecd52f8adf5..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipControlsViewController.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (C) 2019 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.pip.tv; - -import android.app.PendingIntent; -import android.app.RemoteAction; -import android.graphics.Color; -import android.media.session.MediaController; -import android.media.session.PlaybackState; -import android.os.Handler; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; - -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.pip.Pip; -import com.android.wm.shell.R; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Controller for {@link PipControlsView}. - */ -public class PipControlsViewController { - private static final String TAG = PipControlsViewController.class.getSimpleName(); - - private static final float DISABLED_ACTION_ALPHA = 0.54f; - - private final PipControlsView mView; - private final LayoutInflater mLayoutInflater; - private final Handler mHandler; - private final Optional<Pip> mPipOptional; - private final PipControlButtonView mPlayPauseButtonView; - private MediaController mMediaController; - private PipControlButtonView mFocusedChild; - private Listener mListener; - private ArrayList<PipControlButtonView> mCustomButtonViews = new ArrayList<>(); - private List<RemoteAction> mCustomActions = new ArrayList<>(); - - public PipControlsView getView() { - return mView; - } - - /** - * An interface to listen user action. - */ - public interface Listener { - /** - * Called when a user clicks close PIP button. - */ - void onClosed(); - } - - private View.OnAttachStateChangeListener - mOnAttachStateChangeListener = - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - updateMediaController(); - mPipOptional.ifPresent( - pip -> pip.addMediaListener(mPipMediaListener)); - } - - @Override - public void onViewDetachedFromWindow(View v) { - mPipOptional.ifPresent( - pip -> pip.removeMediaListener(mPipMediaListener)); - } - }; - - private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { - @Override - public void onPlaybackStateChanged(PlaybackState state) { - updateUserActions(); - } - }; - - private final PipController.MediaListener mPipMediaListener = this::updateMediaController; - - private final View.OnFocusChangeListener - mFocusChangeListener = - new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean hasFocus) { - if (hasFocus) { - mFocusedChild = (PipControlButtonView) view; - } else if (mFocusedChild == view) { - mFocusedChild = null; - } - } - }; - - - public PipControlsViewController(PipControlsView view, Optional<Pip> pipOptional, - LayoutInflater layoutInflater, @Main Handler handler) { - super(); - mView = view; - mPipOptional = pipOptional; - mLayoutInflater = layoutInflater; - mHandler = handler; - - mView.addOnAttachStateChangeListener(mOnAttachStateChangeListener); - if (mView.isAttachedToWindow()) { - mOnAttachStateChangeListener.onViewAttachedToWindow(mView); - } - - View fullButtonView = mView.getFullButtonView(); - fullButtonView.setOnFocusChangeListener(mFocusChangeListener); - fullButtonView.setOnClickListener( - v -> mPipOptional.ifPresent(pip -> pip.movePipToFullscreen()) - ); - - View closeButtonView = mView.getCloseButtonView(); - closeButtonView.setOnFocusChangeListener(mFocusChangeListener); - closeButtonView.setOnClickListener(v -> { - mPipOptional.ifPresent(pip -> pip.closePip()); - if (mListener != null) { - mListener.onClosed(); - } - }); - - - mPlayPauseButtonView = mView.getPlayPauseButtonView(); - mPlayPauseButtonView.setOnFocusChangeListener(mFocusChangeListener); - mPlayPauseButtonView.setOnClickListener(v -> { - if (mMediaController == null || mMediaController.getPlaybackState() == null) { - return; - } - mPipOptional.ifPresent(pip -> { - if (pip.getPlaybackState() == PipController.PLAYBACK_STATE_PAUSED) { - mMediaController.getTransportControls().play(); - } else if (pip.getPlaybackState() == PipController.PLAYBACK_STATE_PLAYING) { - mMediaController.getTransportControls().pause(); - } - }); - - // View will be updated later in {@link mMediaControllerCallback} - }); - } - - private void updateMediaController() { - AtomicReference<MediaController> newController = new AtomicReference<>(); - mPipOptional.ifPresent(pip -> newController.set(pip.getMediaController())); - - if (newController.get() == null || mMediaController == newController.get()) { - return; - } - if (mMediaController != null) { - mMediaController.unregisterCallback(mMediaControllerCallback); - } - mMediaController = newController.get(); - if (mMediaController != null) { - mMediaController.registerCallback(mMediaControllerCallback); - } - updateUserActions(); - } - - /** - * Updates the actions for the PIP. If there are no custom actions, then the media session - * actions are shown. - */ - private void updateUserActions() { - if (!mCustomActions.isEmpty()) { - // Ensure we have as many buttons as actions - while (mCustomButtonViews.size() < mCustomActions.size()) { - PipControlButtonView buttonView = (PipControlButtonView) mLayoutInflater.inflate( - R.layout.tv_pip_custom_control, mView, false); - mView.addView(buttonView); - mCustomButtonViews.add(buttonView); - } - - // Update the visibility of all views - for (int i = 0; i < mCustomButtonViews.size(); i++) { - mCustomButtonViews.get(i).setVisibility( - i < mCustomActions.size() ? View.VISIBLE : View.GONE); - } - - // Update the state and visibility of the action buttons, and hide the rest - for (int i = 0; i < mCustomActions.size(); i++) { - final RemoteAction action = mCustomActions.get(i); - PipControlButtonView actionView = mCustomButtonViews.get(i); - - // TODO: Check if the action drawable has changed before we reload it - action.getIcon().loadDrawableAsync(mView.getContext(), d -> { - d.setTint(Color.WHITE); - actionView.setImageDrawable(d); - }, mHandler); - actionView.setText(action.getContentDescription()); - if (action.isEnabled()) { - actionView.setOnClickListener(v -> { - try { - action.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - Log.w(TAG, "Failed to send action", e); - } - }); - } - actionView.setEnabled(action.isEnabled()); - actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); - } - - // Hide the media session buttons - mPlayPauseButtonView.setVisibility(View.GONE); - } else { - AtomicInteger state = new AtomicInteger(PipController.STATE_UNKNOWN); - mPipOptional.ifPresent(pip -> state.set(pip.getPlaybackState())); - if (state.get() == PipController.STATE_UNKNOWN - || state.get() == PipController.PLAYBACK_STATE_UNAVAILABLE) { - mPlayPauseButtonView.setVisibility(View.GONE); - } else { - mPlayPauseButtonView.setVisibility(View.VISIBLE); - if (state.get() == PipController.PLAYBACK_STATE_PLAYING) { - mPlayPauseButtonView.setImageResource(R.drawable.pip_ic_pause_white); - mPlayPauseButtonView.setText(R.string.pip_pause); - } else { - mPlayPauseButtonView.setImageResource(R.drawable.pip_ic_play_arrow_white); - mPlayPauseButtonView.setText(R.string.pip_play); - } - } - - // Hide all the custom action buttons - for (int i = 0; i < mCustomButtonViews.size(); i++) { - mCustomButtonViews.get(i).setVisibility(View.GONE); - } - } - } - - - /** - * Sets the {@link Listener} to listen user actions. - */ - public void setListener(Listener listener) { - mListener = listener; - } - - - /** - * Updates the set of activity-defined actions. - */ - public void setActions(List<? extends RemoteAction> actions) { - mCustomActions.clear(); - mCustomActions.addAll(actions); - updateUserActions(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java deleted file mode 100644 index 7e812d9ca8a1..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipMenuActivity.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (C) 2016 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.pip.tv; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.app.Activity; -import android.app.RemoteAction; -import android.content.Intent; -import android.content.pm.ParceledListSlice; -import android.os.Bundle; -import android.util.Log; - -import com.android.systemui.pip.Pip; -import com.android.systemui.pip.tv.dagger.TvPipComponent; -import com.android.wm.shell.R; - -import java.util.Collections; -import java.util.Optional; - -import javax.inject.Inject; - -/** - * Activity to show the PIP menu to control PIP. - */ - -public class PipMenuActivity extends Activity implements PipController.Listener { - private static final String TAG = "PipMenuActivity"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - static final String EXTRA_CUSTOM_ACTIONS = "custom_actions"; - - private final TvPipComponent.Builder mPipComponentBuilder; - private TvPipComponent mTvPipComponent; - private final Optional<Pip> mPipOptional; - - private Animator mFadeInAnimation; - private Animator mFadeOutAnimation; - private boolean mRestorePipSizeWhenClose; - private PipControlsViewController mPipControlsViewController; - - @Inject - public PipMenuActivity(TvPipComponent.Builder pipComponentBuilder, - Optional<Pip> pipOptional) { - super(); - mPipComponentBuilder = pipComponentBuilder; - mPipOptional = pipOptional; - } - - @Override - protected void onCreate(Bundle bundle) { - if (DEBUG) Log.d(TAG, "onCreate()"); - - super.onCreate(bundle); - mPipOptional.ifPresent(pip -> { - if (!pip.isPipShown()) { - finish(); - } - }); - setContentView(R.layout.tv_pip_menu); - mTvPipComponent = mPipComponentBuilder.pipControlsView( - findViewById(R.id.pip_controls)).build(); - mPipControlsViewController = mTvPipComponent.getPipControlsViewController(); - - mPipOptional.ifPresent(pip -> pip.addListener(this)); - - mRestorePipSizeWhenClose = true; - mFadeInAnimation = AnimatorInflater.loadAnimator( - this, R.anim.tv_pip_menu_fade_in_animation); - mFadeInAnimation.setTarget(mPipControlsViewController.getView()); - mFadeOutAnimation = AnimatorInflater.loadAnimator( - this, R.anim.tv_pip_menu_fade_out_animation); - mFadeOutAnimation.setTarget(mPipControlsViewController.getView()); - - onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS)); - } - - @Override - protected void onNewIntent(Intent intent) { - if (DEBUG) Log.d(TAG, "onNewIntent(), intent=" + intent); - super.onNewIntent(intent); - - onPipMenuActionsChanged(getIntent().getParcelableExtra(EXTRA_CUSTOM_ACTIONS)); - } - - private void restorePipAndFinish() { - if (DEBUG) Log.d(TAG, "restorePipAndFinish()"); - - if (mRestorePipSizeWhenClose) { - if (DEBUG) Log.d(TAG, " > restoring to the default position"); - - // When PIP menu activity is closed, restore to the default position. - mPipOptional.ifPresent(pip -> pip.resizePinnedStack(PipController.STATE_PIP)); - } - finish(); - } - - @Override - public void onResume() { - if (DEBUG) Log.d(TAG, "onResume()"); - - super.onResume(); - mFadeInAnimation.start(); - } - - @Override - public void onPause() { - if (DEBUG) Log.d(TAG, "onPause()"); - - super.onPause(); - mFadeOutAnimation.start(); - restorePipAndFinish(); - } - - @Override - protected void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy()"); - - super.onDestroy(); - mPipOptional.ifPresent(pip -> pip.removeListener(this)); - mPipOptional.ifPresent(pip -> pip.resumePipResizing( - PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH)); - } - - @Override - public void onBackPressed() { - if (DEBUG) Log.d(TAG, "onBackPressed()"); - - restorePipAndFinish(); - } - - @Override - public void onPipEntered(String packageName) { - if (DEBUG) Log.d(TAG, "onPipEntered(), packageName=" + packageName); - } - - @Override - public void onPipActivityClosed() { - if (DEBUG) Log.d(TAG, "onPipActivityClosed()"); - - finish(); - } - - @Override - public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) { - if (DEBUG) Log.d(TAG, "onPipMenuActionsChanged()"); - - boolean hasCustomActions = actions != null && !actions.getList().isEmpty(); - mPipControlsViewController.setActions( - hasCustomActions ? actions.getList() : Collections.emptyList()); - } - - @Override - public void onShowPipMenu() { - if (DEBUG) Log.d(TAG, "onShowPipMenu()"); - } - - @Override - public void onMoveToFullscreen() { - if (DEBUG) Log.d(TAG, "onMoveToFullscreen()"); - - // Moving PIP to fullscreen is implemented by resizing PINNED_STACK with null bounds. - // This conflicts with restoring PIP position, so disable it. - mRestorePipSizeWhenClose = false; - finish(); - } - - @Override - public void onPipResizeAboutToStart() { - if (DEBUG) Log.d(TAG, "onPipResizeAboutToStart()"); - - finish(); - mPipOptional.ifPresent(pip -> pip.suspendPipResizing( - PipController.SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH)); - } - - @Override - public void finish() { - if (DEBUG) Log.d(TAG, "finish()", new RuntimeException()); - - super.finish(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java deleted file mode 100644 index 78569edf009d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipNotification.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (C) 2017 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.pip.tv; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.RemoteAction; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.PlaybackState; -import android.text.TextUtils; -import android.util.Log; - -import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; -import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.util.NotificationChannels; -import com.android.wm.shell.R; - -/** - * A notification that informs users that PIP is running and also provides PIP controls. - * <p>Once it's created, it will manage the PIP notification UI by itself except for handling - * configuration changes. - */ -public class PipNotification { - private static final String TAG = "PipNotification"; - private static final String NOTIFICATION_TAG = PipNotification.class.getSimpleName(); - private static final boolean DEBUG = PipController.DEBUG; - - private static final String ACTION_MENU = "PipNotification.menu"; - private static final String ACTION_CLOSE = "PipNotification.close"; - - private final PackageManager mPackageManager; - - private final PipController mPipController; - - private final NotificationManager mNotificationManager; - private final Notification.Builder mNotificationBuilder; - - private MediaController mMediaController; - private String mDefaultTitle; - private int mDefaultIconResId; - - /** Package name for the application that owns PiP window. */ - private String mPackageName; - private boolean mNotified; - private String mMediaTitle; - private Bitmap mArt; - - private PipController.Listener mPipListener = new PipController.Listener() { - @Override - public void onPipEntered(String packageName) { - mPackageName = packageName; - updateMediaControllerMetadata(); - notifyPipNotification(); - } - - @Override - public void onPipActivityClosed() { - dismissPipNotification(); - mPackageName = null; - } - - @Override - public void onShowPipMenu() { - // no-op. - } - - @Override - public void onPipMenuActionsChanged(ParceledListSlice<RemoteAction> actions) { - // no-op. - } - - @Override - public void onMoveToFullscreen() { - dismissPipNotification(); - mPackageName = null; - } - - @Override - public void onPipResizeAboutToStart() { - // no-op. - } - }; - - private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() { - @Override - public void onPlaybackStateChanged(PlaybackState state) { - if (updateMediaControllerMetadata() && mNotified) { - // update notification - notifyPipNotification(); - } - } - - @Override - public void onMetadataChanged(MediaMetadata metadata) { - if (updateMediaControllerMetadata() && mNotified) { - // update notification - notifyPipNotification(); - } - } - }; - - private final PipController.MediaListener mPipMediaListener = - new PipController.MediaListener() { - @Override - public void onMediaControllerChanged() { - MediaController newController = mPipController.getMediaController(); - if (newController == null || mMediaController == newController) { - return; - } - if (mMediaController != null) { - mMediaController.unregisterCallback(mMediaControllerCallback); - } - mMediaController = newController; - if (mMediaController != null) { - mMediaController.registerCallback(mMediaControllerCallback); - } - if (updateMediaControllerMetadata() && mNotified) { - // update notification - notifyPipNotification(); - } - } - }; - - private final BroadcastReceiver mEventReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (DEBUG) { - Log.d(TAG, "Received " + intent.getAction() + " from the notification UI"); - } - switch (intent.getAction()) { - case ACTION_MENU: - mPipController.showPictureInPictureMenu(); - break; - case ACTION_CLOSE: - mPipController.closePip(); - break; - } - } - }; - - public PipNotification(Context context, BroadcastDispatcher broadcastDispatcher, - PipController pipController) { - mPackageManager = context.getPackageManager(); - - mNotificationManager = (NotificationManager) context.getSystemService( - Context.NOTIFICATION_SERVICE); - - mNotificationBuilder = new Notification.Builder(context, NotificationChannels.TVPIP) - .setLocalOnly(true) - .setOngoing(false) - .setCategory(Notification.CATEGORY_SYSTEM) - .extend(new Notification.TvExtender() - .setContentIntent(createPendingIntent(context, ACTION_MENU)) - .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE))); - - mPipController = pipController; - pipController.addListener(mPipListener); - pipController.addMediaListener(mPipMediaListener); - - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(ACTION_MENU); - intentFilter.addAction(ACTION_CLOSE); - broadcastDispatcher.registerReceiver(mEventReceiver, intentFilter); - - onConfigurationChanged(context); - } - - /** - * Called by {@link PipController} when the configuration is changed. - */ - void onConfigurationChanged(Context context) { - Resources res = context.getResources(); - mDefaultTitle = res.getString(R.string.pip_notification_unknown_title); - mDefaultIconResId = R.drawable.pip_icon; - if (mNotified) { - // update notification - notifyPipNotification(); - } - } - - private void notifyPipNotification() { - mNotified = true; - mNotificationBuilder - .setShowWhen(true) - .setWhen(System.currentTimeMillis()) - .setSmallIcon(mDefaultIconResId) - .setContentTitle(getNotificationTitle()); - if (mArt != null) { - mNotificationBuilder.setStyle(new Notification.BigPictureStyle() - .bigPicture(mArt)); - } else { - mNotificationBuilder.setStyle(null); - } - mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, - mNotificationBuilder.build()); - } - - private void dismissPipNotification() { - mNotified = false; - mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); - } - - private boolean updateMediaControllerMetadata() { - String title = null; - Bitmap art = null; - if (mPipController.getMediaController() != null) { - MediaMetadata metadata = mPipController.getMediaController().getMetadata(); - if (metadata != null) { - title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE); - if (TextUtils.isEmpty(title)) { - title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE); - } - art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - if (art == null) { - art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART); - } - } - } - if (!TextUtils.equals(title, mMediaTitle) || art != mArt) { - mMediaTitle = title; - mArt = art; - return true; - } - return false; - } - - - private String getNotificationTitle() { - if (!TextUtils.isEmpty(mMediaTitle)) { - return mMediaTitle; - } - - final String applicationTitle = getApplicationLabel(mPackageName); - if (!TextUtils.isEmpty(applicationTitle)) { - return applicationTitle; - } - - return mDefaultTitle; - } - - private String getApplicationLabel(String packageName) { - try { - final ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0); - return mPackageManager.getApplicationLabel(appInfo).toString(); - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } - - private static PendingIntent createPendingIntent(Context context, String action) { - return PendingIntent.getBroadcast(context, 0, - new Intent(action), PendingIntent.FLAG_CANCEL_CURRENT); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/dagger/TvPipComponent.java b/packages/SystemUI/src/com/android/systemui/pip/tv/dagger/TvPipComponent.java deleted file mode 100644 index 8e8b7f37b8d6..000000000000 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/dagger/TvPipComponent.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2019 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.pip.tv.dagger; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import com.android.systemui.pip.tv.PipControlsView; -import com.android.systemui.pip.tv.PipControlsViewController; -import com.android.systemui.statusbar.phone.dagger.StatusBarComponent; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; - -import javax.inject.Scope; - -import dagger.BindsInstance; -import dagger.Subcomponent; - -/** - * Component for injecting into Pip related classes. - */ -@Subcomponent -public interface TvPipComponent { - /** - * Builder for {@link StatusBarComponent}. - */ - @Subcomponent.Builder - interface Builder { - @BindsInstance - TvPipComponent.Builder pipControlsView(PipControlsView pipControlsView); - TvPipComponent build(); - } - - /** - * Scope annotation for singleton items within the PipComponent. - */ - @Documented - @Retention(RUNTIME) - @Scope - @interface PipScope {} - - /** - * Creates a StatusBarWindowViewController. - */ - @TvPipComponent.PipScope - PipControlsViewController getPipControlsViewController(); -} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt index 0fbd73b615ce..f56e6cdf5cb7 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -16,25 +16,23 @@ package com.android.systemui.privacy -import android.app.ActivityManager import android.app.AppOpsManager -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.UserInfo import android.os.UserHandle -import android.os.UserManager import android.provider.DeviceConfig import com.android.internal.annotations.VisibleForTesting import com.android.internal.config.sysui.SystemUiDeviceConfigFlags import com.android.systemui.Dumpable import com.android.systemui.appops.AppOpItem import com.android.systemui.appops.AppOpsController -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.settings.UserTracker import com.android.systemui.util.DeviceConfigProxy import com.android.systemui.util.concurrency.DelayableExecutor import java.io.FileDescriptor @@ -48,9 +46,8 @@ class PrivacyItemController @Inject constructor( private val appOpsController: AppOpsController, @Main uiExecutor: DelayableExecutor, @Background private val bgExecutor: Executor, - private val broadcastDispatcher: BroadcastDispatcher, private val deviceConfigProxy: DeviceConfigProxy, - private val userManager: UserManager, + private val userTracker: UserTracker, dumpManager: DumpManager ) : Dumpable { @@ -153,13 +150,16 @@ class PrivacyItemController @Inject constructor( } @VisibleForTesting - internal var userSwitcherReceiver = Receiver() - set(value) { - unregisterReceiver() - field = value - if (listening) registerReceiver() + internal var userTrackerCallback = object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + update(true) } + override fun onProfilesChanged(profiles: List<UserInfo>) { + update(true) + } + } + init { deviceConfigProxy.addOnPropertiesChangedListener( DeviceConfig.NAMESPACE_PRIVACY, @@ -168,20 +168,18 @@ class PrivacyItemController @Inject constructor( dumpManager.registerDumpable(TAG, this) } - private fun unregisterReceiver() { - broadcastDispatcher.unregisterReceiver(userSwitcherReceiver) + private fun unregisterListener() { + userTracker.removeCallback(userTrackerCallback) } private fun registerReceiver() { - broadcastDispatcher.registerReceiver(userSwitcherReceiver, intentFilter, - null /* handler */, UserHandle.ALL) + userTracker.addCallback(userTrackerCallback, bgExecutor) } private fun update(updateUsers: Boolean) { bgExecutor.execute { if (updateUsers) { - val currentUser = ActivityManager.getCurrentUser() - currentUserIds = userManager.getProfiles(currentUser).map { it.id } + currentUserIds = userTracker.userProfiles.map { it.id } } updateListAndNotifyChanges.run() } @@ -206,7 +204,7 @@ class PrivacyItemController @Inject constructor( update(true) } else { appOpsController.removeCallback(OPS, cb) - unregisterReceiver() + unregisterListener() // Make sure that we remove all indicators and notify listeners if we are not // listening anymore due to indicators being disabled update(false) @@ -275,14 +273,6 @@ class PrivacyItemController @Inject constructor( fun onFlagMicCameraChanged(flag: Boolean) {} } - internal inner class Receiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intentFilter.hasAction(intent.action)) { - update(true) - } - } - } - private class NotifyChangesToCallback( private val callback: Callback?, private val list: List<PrivacyItem> diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java index 52a2cecec6b1..0053fea35262 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java @@ -2,6 +2,7 @@ package com.android.systemui.qs; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Animatable2; import android.graphics.drawable.AnimatedVectorDrawable; @@ -31,9 +32,6 @@ public class PageIndicator extends ViewGroup { private static final long ANIMATION_DURATION = 250; - // The size of a single dot in relation to the whole animation. - private static final float SINGLE_SCALE = .4f; - private static final float MINOR_ALPHA = .42f; private final ArrayList<Integer> mQueuedPositions = new ArrayList<>(); @@ -75,11 +73,10 @@ public class PageIndicator extends ViewGroup { } array.recycle(); - mPageIndicatorWidth = - (int) mContext.getResources().getDimension(R.dimen.qs_page_indicator_width); - mPageIndicatorHeight = - (int) mContext.getResources().getDimension(R.dimen.qs_page_indicator_height); - mPageDotWidth = (int) (mPageIndicatorWidth * SINGLE_SCALE); + Resources res = context.getResources(); + mPageIndicatorWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_width); + mPageIndicatorHeight = res.getDimensionPixelSize(R.dimen.qs_page_indicator_height); + mPageDotWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_dot_width); } public void setNumPages(int numPages) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 22c735d5fa11..04f379ef35ea 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -177,6 +177,16 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { } @Override + public void endFakeDrag() { + try { + super.endFakeDrag(); + } catch (NullPointerException e) { + // Not sure what's going on. Let's log it + Log.e(TAG, "endFakeDrag called without velocityTracker", e); + } + } + + @Override public void computeScroll() { if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { if (!isFakeDragging()) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index eba4465018ab..7e2433a1fd33 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.Point; import android.util.AttributeSet; +import android.util.Pair; import android.view.View; import android.widget.FrameLayout; @@ -31,7 +32,7 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.systemui.R; import com.android.systemui.qs.customize.QSCustomizer; -import com.android.systemui.util.animation.PhysicsAnimator; +import com.android.wm.shell.animation.PhysicsAnimator; /** * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader} @@ -282,7 +283,7 @@ public class QSContainerImpl extends FrameLayout { View view = getChildAt(i); if (view == mStatusBarBackground || view == mBackgroundGradient || view == mQSCustomizer) { - // Some views are always full width + // Some views are always full width or have dependent padding continue; } LayoutParams lp = (LayoutParams) view.getLayoutParams(); @@ -291,6 +292,9 @@ public class QSContainerImpl extends FrameLayout { if (view == mQSPanelContainer) { // QS panel lays out some of its content full width mQSPanel.setContentMargins(mContentPaddingStart, mContentPaddingEnd); + Pair<Integer, Integer> margins = mQSPanel.getVisualSideMargins(); + // Apply paddings based on QSPanel + mQSCustomizer.setContentPaddings(margins.first, margins.second); } else if (view == mHeader) { // The header contains the QQS panel which needs to have special padding, to // visually align them. diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java index fa3328417bd6..1e239b1e9ec9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java @@ -17,30 +17,44 @@ package com.android.systemui.qs; import com.android.systemui.R; +import com.android.systemui.util.ViewController; import javax.inject.Inject; -public class QSContainerImplController { - private final QSContainerImpl mView; +class QSContainerImplController extends ViewController<QSContainerImpl> { private final QuickStatusBarHeaderController mQuickStatusBarHeaderController; private QSContainerImplController(QSContainerImpl view, QuickStatusBarHeaderController.Builder quickStatusBarHeaderControllerBuilder) { - mView = view; + super(view); mQuickStatusBarHeaderController = quickStatusBarHeaderControllerBuilder .setQuickStatusBarHeader(mView.findViewById(R.id.header)).build(); } + @Override + public void init() { + super.init(); + mQuickStatusBarHeaderController.init(); + } + public void setListening(boolean listening) { mQuickStatusBarHeaderController.setListening(listening); } - public static class Builder { + @Override + protected void onViewAttached() { + } + + @Override + protected void onViewDetached() { + } + + static class Builder { private final QuickStatusBarHeaderController.Builder mQuickStatusBarHeaderControllerBuilder; private QSContainerImpl mView; @Inject - public Builder( + Builder( QuickStatusBarHeaderController.Builder quickStatusBarHeaderControllerBuilder) { mQuickStatusBarHeaderControllerBuilder = quickStatusBarHeaderControllerBuilder; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java index 6e4ab9a3323a..84563a078447 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java @@ -20,6 +20,8 @@ import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; @@ -34,6 +36,7 @@ import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; @@ -57,6 +60,7 @@ import com.android.systemui.R; import com.android.systemui.R.dimen; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.TouchAnimator.Builder; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.MultiUserSwitch; import com.android.systemui.statusbar.phone.SettingsButton; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -75,6 +79,7 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, private final ActivityStarter mActivityStarter; private final UserInfoController mUserInfoController; private final DeviceProvisionedController mDeviceProvisionedController; + private final UserTracker mUserTracker; private SettingsButton mSettingsButton; protected View mSettingsContainer; private PageIndicator mPageIndicator; @@ -115,11 +120,12 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, @Inject public QSFooterImpl(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, ActivityStarter activityStarter, UserInfoController userInfoController, - DeviceProvisionedController deviceProvisionedController) { + DeviceProvisionedController deviceProvisionedController, UserTracker userTracker) { super(context, attrs); mActivityStarter = activityStarter; mUserInfoController = userInfoController; mDeviceProvisionedController = deviceProvisionedController; + mUserTracker = userTracker; } @VisibleForTesting @@ -127,7 +133,8 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, this(context, attrs, Dependency.get(ActivityStarter.class), Dependency.get(UserInfoController.class), - Dependency.get(DeviceProvisionedController.class)); + Dependency.get(DeviceProvisionedController.class), + Dependency.get(UserTracker.class)); } @Override @@ -150,6 +157,19 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mActionsContainer = findViewById(R.id.qs_footer_actions_container); mEditContainer = findViewById(R.id.qs_footer_actions_edit_container); mBuildText = findViewById(R.id.build); + mBuildText.setOnLongClickListener(view -> { + CharSequence buildText = mBuildText.getText(); + if (!TextUtils.isEmpty(buildText)) { + ClipboardManager service = + mUserTracker.getUserContext().getSystemService(ClipboardManager.class); + String label = mContext.getString(R.string.build_number_clip_data_label); + service.setPrimaryClip(ClipData.newPlainText(label, buildText)); + Toast.makeText(mContext, R.string.build_number_copy_toast, Toast.LENGTH_SHORT) + .show(); + return true; + } + return false; + }); // RenderThread is doing more harm than good when touching the header (to expand quick // settings), so disable it for this view @@ -176,6 +196,7 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mBuildText.setSelected(true); mShouldShowBuildText = true; } else { + mBuildText.setText(null); mShouldShowBuildText = false; mBuildText.setSelected(false); } @@ -317,12 +338,14 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mMultiUserSwitch.setClickable(mMultiUserSwitch.getVisibility() == View.VISIBLE); mEdit.setClickable(mEdit.getVisibility() == View.VISIBLE); mSettingsButton.setClickable(mSettingsButton.getVisibility() == View.VISIBLE); + mBuildText.setLongClickable(mBuildText.getVisibility() == View.VISIBLE); } private void updateVisibilities() { mSettingsContainer.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility( - TunerService.isTunerEnabled(mContext) ? View.VISIBLE : View.INVISIBLE); + TunerService.isTunerEnabled(mContext, mUserTracker.getUserHandle()) ? View.VISIBLE + : View.INVISIBLE); final boolean isDemo = UserManager.isDeviceInDemoMode(mContext); mMultiUserSwitch.setVisibility(showUserSwitcher() ? View.VISIBLE : View.INVISIBLE); mEditContainer.setVisibility(isDemo || !mExpanded ? View.INVISIBLE : View.VISIBLE); @@ -376,15 +399,16 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, : MetricsProto.MetricsEvent.ACTION_QS_COLLAPSED_SETTINGS_LAUNCH); if (mSettingsButton.isTunerClick()) { mActivityStarter.postQSRunnableDismissingKeyguard(() -> { - if (TunerService.isTunerEnabled(mContext)) { - TunerService.showResetRequest(mContext, () -> { - // Relaunch settings so that the tuner disappears. - startSettingsActivity(); - }); + if (TunerService.isTunerEnabled(mContext, mUserTracker.getUserHandle())) { + TunerService.showResetRequest(mContext, mUserTracker.getUserHandle(), + () -> { + // Relaunch settings so that the tuner disappears. + startSettingsActivity(); + }); } else { Toast.makeText(getContext(), R.string.tuner_toast, Toast.LENGTH_LONG).show(); - TunerService.setTunerEnabled(mContext, true); + TunerService.setTunerEnabled(mContext, mUserTracker.getUserHandle(), true); } startSettingsActivity(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index f1bb8996e181..3a783653a2d8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -142,7 +142,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mQSContainerImplController = mQSContainerImplControllerBuilder .setQSContainerImpl((QSContainerImpl) view) .build(); - + mQSContainerImplController.init(); mQSDetail.setQsPanel(mQSPanel, mHeader, (View) mFooter); mQSAnimator = new QSAnimator(this, mHeader.findViewById(R.id.quick_qs_panel), mQSPanel); @@ -367,14 +367,13 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (DEBUG) Log.d(TAG, "setListening " + listening); mListening = listening; mQSContainerImplController.setListening(listening); - mHeader.setListening(listening); mFooter.setListening(listening); mQSPanel.setListening(mListening, mQsExpanded); } @Override public void setHeaderListening(boolean listening) { - mHeader.setListening(listening); + mQSContainerImplController.setListening(listening); mFooter.setListening(listening); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index 290ab8594fc0..000fd1c4bd2e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -30,6 +30,7 @@ public interface QSHost { void openPanels(); Context getContext(); Context getUserContext(); + int getUserId(); UiEventLogger getUiEventLogger(); Collection<QSTile> getTiles(); void addCallback(Callback callback); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 1eea4337eac1..61c6d3a629ab 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -25,12 +25,12 @@ import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.PointF; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; +import android.util.Pair; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -57,6 +57,7 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.BrightnessController; import com.android.systemui.settings.ToggleSliderView; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener; import com.android.systemui.tuner.TunerService; @@ -114,6 +115,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private final QSLogger mQSLogger; protected final UiEventLogger mUiEventLogger; protected QSTileHost mHost; + private final UserTracker mUserTracker; @Nullable protected QSSecurityFooter mSecurityFooter; @@ -157,7 +159,8 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, MediaHost mediaHost, - UiEventLogger uiEventLogger + UiEventLogger uiEventLogger, + UserTracker userTracker ) { super(context, attrs); mUsingMediaPlayer = useQsMediaPlayer(context); @@ -173,6 +176,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mDumpManager = dumpManager; mBroadcastDispatcher = broadcastDispatcher; mUiEventLogger = uiEventLogger; + mUserTracker = userTracker; setOrientation(VERTICAL); @@ -221,7 +225,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } protected void addSecurityFooter() { - mSecurityFooter = new QSSecurityFooter(this, mContext); + mSecurityFooter = new QSSecurityFooter(this, mContext, mUserTracker); } protected void addViewsAboveTiles() { @@ -1080,6 +1084,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne updateTileLayoutMargins(); } + public Pair<Integer, Integer> getVisualSideMargins() { + return new Pair(mVisualMarginStart, mUsingHorizontalLayout ? 0 : mVisualMarginEnd); + } + private void updateTileLayoutMargins() { int marginEnd = mVisualMarginEnd; if (mUsingHorizontalLayout) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index afc5be4e6c2f..0891972c11d2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -15,7 +15,6 @@ */ package com.android.systemui.qs; -import android.app.ActivityManager; import android.app.AlertDialog; import android.app.admin.DevicePolicyEventLogger; import android.content.Context; @@ -45,6 +44,7 @@ import com.android.systemui.Dependency; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.SecurityController; @@ -61,8 +61,7 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic private final SecurityController mSecurityController; private final ActivityStarter mActivityStarter; private final Handler mMainHandler; - - private final UserManager mUm; + private final UserTracker mUserTracker; private AlertDialog mDialog; private QSTileHost mHost; @@ -73,7 +72,7 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic private int mFooterTextId; private int mFooterIconId; - public QSSecurityFooter(QSPanel qsPanel, Context context) { + public QSSecurityFooter(QSPanel qsPanel, Context context, UserTracker userTracker) { mRootView = LayoutInflater.from(context) .inflate(R.layout.quick_settings_footer, qsPanel, false); mRootView.setOnClickListener(this); @@ -85,7 +84,7 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic mActivityStarter = Dependency.get(ActivityStarter.class); mSecurityController = Dependency.get(SecurityController.class); mHandler = new H(Dependency.get(Dependency.BG_LOOPER)); - mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mUserTracker = userTracker; } public void setHostEnvironment(QSTileHost host) { @@ -138,7 +137,7 @@ public class QSSecurityFooter implements OnClickListener, DialogInterface.OnClic private void handleRefreshState() { final boolean isDeviceManaged = mSecurityController.isDeviceManaged(); - final UserInfo currentUser = mUm.getUserInfo(ActivityManager.getCurrentUser()); + final UserInfo currentUser = mUserTracker.getUserInfo(); final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null && currentUser.isDemo(); final boolean hasWorkProfile = mSecurityController.hasWorkProfile(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 9a63a56b2c8e..0d0d01249c3d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -14,7 +14,6 @@ package com.android.systemui.qs; -import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -49,6 +48,7 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServices; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.StatusBar; @@ -99,6 +99,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D private int mCurrentUser; private final Optional<StatusBar> mStatusBarOptional; private Context mUserContext; + private UserTracker mUserTracker; @Inject public QSTileHost(Context context, @@ -113,7 +114,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D BroadcastDispatcher broadcastDispatcher, Optional<StatusBar> statusBarOptional, QSLogger qsLogger, - UiEventLogger uiEventLogger) { + UiEventLogger uiEventLogger, + UserTracker userTracker) { mIconController = iconController; mContext = context; mUserContext = context; @@ -125,12 +127,13 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D mBroadcastDispatcher = broadcastDispatcher; mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID); - mServices = new TileServices(this, bgLooper, mBroadcastDispatcher); + mServices = new TileServices(this, bgLooper, mBroadcastDispatcher, userTracker); mStatusBarOptional = statusBarOptional; mQsFactories.add(defaultFactory); pluginManager.addPluginListener(this, QSFactory.class, true); mDumpManager.registerDumpable(TAG, this); + mUserTracker = userTracker; mainHandler.post(() -> { // This is technically a hack to avoid circular dependency of @@ -230,6 +233,11 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D } @Override + public int getUserId() { + return mCurrentUser; + } + + @Override public TileServices getTileServices() { return mServices; } @@ -248,9 +256,9 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); } final List<String> tileSpecs = loadTileSpecs(mContext, newValue); - int currentUser = ActivityManager.getCurrentUser(); + int currentUser = mUserTracker.getUserId(); if (currentUser != mCurrentUser) { - mUserContext = mContext.createContextAsUser(UserHandle.of(currentUser), 0); + mUserContext = mUserTracker.getUserContext(); if (mAutoTiles != null) { mAutoTiles.changeUser(UserHandle.of(currentUser)); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index affb7b91b6a5..ea036f6fe0e5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -38,6 +38,7 @@ import com.android.systemui.plugins.qs.QSTile.SignalState; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.settings.UserTracker; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; @@ -70,9 +71,11 @@ public class QuickQSPanel extends QSPanel { BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, MediaHost mediaHost, - UiEventLogger uiEventLogger + UiEventLogger uiEventLogger, + UserTracker userTracker ) { - super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, uiEventLogger); + super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, uiEventLogger, + userTracker); sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns); applyBottomMargin((View) mRegularTileLayout); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index 2e258d56ece0..a9fbc744b38e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -17,27 +17,16 @@ package com.android.systemui.qs; import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; - import android.annotation.ColorInt; -import android.app.ActivityManager; -import android.app.AlarmManager; +import android.app.AlarmManager.AlarmClockInfo; import android.content.Context; -import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; import android.media.AudioManager; -import android.os.Handler; -import android.os.Looper; -import android.provider.AlarmClock; -import android.provider.Settings; -import android.service.notification.ZenModeConfig; -import android.text.format.DateUtils; import android.util.AttributeSet; -import android.util.Log; import android.util.MathUtils; import android.util.Pair; import android.view.ContextThemeWrapper; @@ -53,90 +42,48 @@ import android.widget.Space; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; -import com.android.internal.logging.UiEventLogger; import com.android.settingslib.Utils; import com.android.systemui.BatteryMeterView; import com.android.systemui.DualToneHandler; import com.android.systemui.Interpolators; import com.android.systemui.R; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; import com.android.systemui.privacy.OngoingPrivacyChip; -import com.android.systemui.privacy.PrivacyChipEvent; -import com.android.systemui.privacy.PrivacyItem; -import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.qs.QSDetail.Callback; -import com.android.systemui.qs.carrier.QSCarrierGroup; -import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager; import com.android.systemui.statusbar.phone.StatusBarWindowView; -import com.android.systemui.statusbar.phone.StatusIconContainer; import com.android.systemui.statusbar.policy.Clock; -import com.android.systemui.statusbar.policy.DateView; -import com.android.systemui.statusbar.policy.NextAlarmController; -import com.android.systemui.statusbar.policy.ZenModeController; -import com.android.systemui.util.RingerModeTracker; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import java.util.Objects; -import javax.inject.Inject; -import javax.inject.Named; - /** * View that contains the top-most bits of the screen (primarily the status bar with date, time, and * battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner * contents. */ -public class QuickStatusBarHeader extends RelativeLayout implements - View.OnClickListener, NextAlarmController.NextAlarmChangeCallback, - ZenModeController.Callback, LifecycleOwner { - private static final String TAG = "QuickStatusBarHeader"; - private static final boolean DEBUG = false; - - /** Delay for auto fading out the long press tooltip after it's fully visible (in ms). */ - private static final long AUTO_FADE_OUT_DELAY_MS = DateUtils.SECOND_IN_MILLIS * 6; - private static final int FADE_ANIMATION_DURATION_MS = 300; - private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0; - public static final int MAX_TOOLTIP_SHOWN_COUNT = 2; +public class QuickStatusBarHeader extends RelativeLayout implements LifecycleOwner { - private final NextAlarmController mAlarmController; - private final ZenModeController mZenController; - private final StatusBarIconController mStatusBarIconController; - private final ActivityStarter mActivityStarter; - - private QSPanel mQsPanel; + public static final int MAX_TOOLTIP_SHOWN_COUNT = 2; private boolean mExpanded; - private boolean mListening; private boolean mQsDisabled; - private QSCarrierGroup mCarrierGroup; protected QuickQSPanel mHeaderQsPanel; - protected QSTileHost mHost; - private TintedIconManager mIconManager; private TouchAnimator mStatusIconsAlphaAnimator; private TouchAnimator mHeaderTextContainerAlphaAnimator; private TouchAnimator mPrivacyChipAlphaAnimator; private DualToneHandler mDualToneHandler; - private final CommandQueue mCommandQueue; private View mSystemIconsView; private View mQuickQsStatusIcons; private View mHeaderTextContainerView; - private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; - private AlarmManager.AlarmClockInfo mNextAlarm; - private ImageView mNextAlarmIcon; /** {@link TextView} containing the actual text indicating when the next alarm will go off. */ private TextView mNextAlarmTextView; @@ -146,20 +93,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements private TextView mRingerModeTextView; private View mRingerContainer; private Clock mClockView; - private DateView mDateView; private OngoingPrivacyChip mPrivacyChip; private Space mSpace; private BatteryMeterView mBatteryRemainingIcon; - private RingerModeTracker mRingerModeTracker; - private boolean mAllIndicatorsEnabled; - private boolean mMicCameraIndicatorsEnabled; - private PrivacyItemController mPrivacyItemController; - private final UiEventLogger mUiEventLogger; // Used for RingerModeTracker private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); - private boolean mHasTopCutout = false; private int mStatusBarPaddingTop = 0; private int mRoundedCornerPadding = 0; private int mContentMarginStart; @@ -169,56 +109,11 @@ public class QuickStatusBarHeader extends RelativeLayout implements private int mCutOutPaddingRight; private float mExpandedHeaderAlpha = 1.0f; private float mKeyguardExpansionFraction; - private boolean mPrivacyChipLogged = false; - - private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() { - @Override - public void onPrivacyItemsChanged(List<PrivacyItem> privacyItems) { - mPrivacyChip.setPrivacyList(privacyItems); - setChipVisibility(!privacyItems.isEmpty()); - } - @Override - public void onFlagAllChanged(boolean flag) { - if (mAllIndicatorsEnabled != flag) { - mAllIndicatorsEnabled = flag; - update(); - } - } - - @Override - public void onFlagMicCameraChanged(boolean flag) { - if (mMicCameraIndicatorsEnabled != flag) { - mMicCameraIndicatorsEnabled = flag; - update(); - } - } - - private void update() { - StatusIconContainer iconContainer = requireViewById(R.id.statusIcons); - iconContainer.setIgnoredSlots(getIgnoredIconSlots()); - setChipVisibility(!mPrivacyChip.getPrivacyList().isEmpty()); - } - }; - - @Inject - public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, - NextAlarmController nextAlarmController, ZenModeController zenModeController, - StatusBarIconController statusBarIconController, - ActivityStarter activityStarter, PrivacyItemController privacyItemController, - CommandQueue commandQueue, RingerModeTracker ringerModeTracker, - UiEventLogger uiEventLogger) { + public QuickStatusBarHeader(Context context, AttributeSet attrs) { super(context, attrs); - mAlarmController = nextAlarmController; - mZenController = zenModeController; - mStatusBarIconController = statusBarIconController; - mActivityStarter = activityStarter; - mPrivacyItemController = privacyItemController; mDualToneHandler = new DualToneHandler( new ContextThemeWrapper(context, R.style.QSHeaderTheme)); - mCommandQueue = commandQueue; - mRingerModeTracker = ringerModeTracker; - mUiEventLogger = uiEventLogger; } @Override @@ -228,11 +123,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements mHeaderQsPanel = findViewById(R.id.quick_qs_panel); mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons); mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons); - StatusIconContainer iconContainer = findViewById(R.id.statusIcons); - // Ignore privacy icons because they show in the space above QQS - iconContainer.addIgnoredSlots(getIgnoredIconSlots()); - iconContainer.setShouldRestrictIcons(false); - mIconManager = new TintedIconManager(iconContainer, mCommandQueue); // Views corresponding to the header info section (e.g. ringer and next alarm). mHeaderTextContainerView = findViewById(R.id.header_text_container); @@ -240,35 +130,18 @@ public class QuickStatusBarHeader extends RelativeLayout implements mNextAlarmIcon = findViewById(R.id.next_alarm_icon); mNextAlarmTextView = findViewById(R.id.next_alarm_text); mNextAlarmContainer = findViewById(R.id.alarm_container); - mNextAlarmContainer.setOnClickListener(this::onClick); mRingerModeIcon = findViewById(R.id.ringer_mode_icon); mRingerModeTextView = findViewById(R.id.ringer_mode_text); mRingerContainer = findViewById(R.id.ringer_container); - mRingerContainer.setOnClickListener(this::onClick); mPrivacyChip = findViewById(R.id.privacy_chip); - mPrivacyChip.setOnClickListener(this::onClick); - mCarrierGroup = findViewById(R.id.carrier_group); - updateResources(); Rect tintArea = new Rect(0, 0, 0, 0); - int colorForeground = Utils.getColorAttrDefaultColor(getContext(), - android.R.attr.colorForeground); - float intensity = getColorIntensity(colorForeground); - int fillColor = mDualToneHandler.getSingleColor(intensity); - // Set light text on the header icons because they will always be on a black background applyDarkness(R.id.clock, tintArea, 0, DarkIconDispatcher.DEFAULT_ICON_TINT); - // Set the correct tint for the status icons so they contrast - mIconManager.setTint(fillColor); - mNextAlarmIcon.setImageTintList(ColorStateList.valueOf(fillColor)); - mRingerModeIcon.setImageTintList(ColorStateList.valueOf(fillColor)); - mClockView = findViewById(R.id.clock); - mClockView.setOnClickListener(this); - mDateView = findViewById(R.id.date); mSpace = findViewById(R.id.space); // Tint for the battery icons are handled in setupHost() @@ -280,33 +153,28 @@ public class QuickStatusBarHeader extends RelativeLayout implements mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); mRingerModeTextView.setSelected(true); mNextAlarmTextView.setSelected(true); + } - mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); - mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); + void onAttach(TintedIconManager iconManager) { + int colorForeground = Utils.getColorAttrDefaultColor(getContext(), + android.R.attr.colorForeground); + float intensity = getColorIntensity(colorForeground); + int fillColor = mDualToneHandler.getSingleColor(intensity); + + // Set the correct tint for the status icons so they contrast + iconManager.setTint(fillColor); + mNextAlarmIcon.setImageTintList(ColorStateList.valueOf(fillColor)); + mRingerModeIcon.setImageTintList(ColorStateList.valueOf(fillColor)); } public QuickQSPanel getHeaderQsPanel() { return mHeaderQsPanel; } - private List<String> getIgnoredIconSlots() { - ArrayList<String> ignored = new ArrayList<>(); - if (getChipEnabled()) { - ignored.add(mContext.getResources().getString( - com.android.internal.R.string.status_bar_camera)); - ignored.add(mContext.getResources().getString( - com.android.internal.R.string.status_bar_microphone)); - if (mAllIndicatorsEnabled) { - ignored.add(mContext.getResources().getString( - com.android.internal.R.string.status_bar_location)); - } - } - - return ignored; - } - - private void updateStatusText() { - boolean changed = updateRingerStatus() || updateAlarmStatus(); + void updateStatusText(int ringerMode, AlarmClockInfo nextAlarm, boolean zenOverridingRinger, + boolean use24HourFormat) { + boolean changed = updateRingerStatus(ringerMode, zenOverridingRinger) + || updateAlarmStatus(nextAlarm, use24HourFormat); if (changed) { boolean alarmVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE; @@ -316,32 +184,17 @@ public class QuickStatusBarHeader extends RelativeLayout implements } } - private void setChipVisibility(boolean chipVisible) { - if (chipVisible && getChipEnabled()) { - mPrivacyChip.setVisibility(View.VISIBLE); - // Makes sure that the chip is logged as viewed at most once each time QS is opened - // mListening makes sure that the callback didn't return after the user closed QS - if (!mPrivacyChipLogged && mListening) { - mPrivacyChipLogged = true; - mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW); - } - } else { - mPrivacyChip.setVisibility(View.GONE); - } - } - - private boolean updateRingerStatus() { + private boolean updateRingerStatus(int ringerMode, boolean zenOverridingRinger) { boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE; CharSequence originalRingerText = mRingerModeTextView.getText(); boolean ringerVisible = false; - if (!ZenModeConfig.isZenOverridingRinger(mZenController.getZen(), - mZenController.getConsolidatedPolicy())) { - if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) { + if (!zenOverridingRinger) { + if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) { mRingerModeIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate); mRingerModeTextView.setText(R.string.qs_status_phone_vibrate); ringerVisible = true; - } else if (mRingerMode == AudioManager.RINGER_MODE_SILENT) { + } else if (ringerMode == AudioManager.RINGER_MODE_SILENT) { mRingerModeIcon.setImageResource(R.drawable.ic_volume_ringer_mute); mRingerModeTextView.setText(R.string.qs_status_phone_muted); ringerVisible = true; @@ -355,14 +208,14 @@ public class QuickStatusBarHeader extends RelativeLayout implements !Objects.equals(originalRingerText, mRingerModeTextView.getText()); } - private boolean updateAlarmStatus() { + private boolean updateAlarmStatus(AlarmClockInfo nextAlarm, boolean use24HourFormat) { boolean isOriginalVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE; CharSequence originalAlarmText = mNextAlarmTextView.getText(); boolean alarmVisible = false; - if (mNextAlarm != null) { + if (nextAlarm != null) { alarmVisible = true; - mNextAlarmTextView.setText(formatNextAlarm(mNextAlarm)); + mNextAlarmTextView.setText(formatNextAlarm(nextAlarm, use24HourFormat)); } mNextAlarmIcon.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); mNextAlarmTextView.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); @@ -409,7 +262,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements setMinimumHeight(sbHeight + qqsHeight); } - private void updateResources() { + void updateResources() { Resources resources = mContext.getResources(); updateMinimumHeight(); @@ -519,17 +372,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements } @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - mRingerModeTracker.getRingerModeInternal().observe(this, ringer -> { - mRingerMode = ringer; - updateStatusText(); - }); - mStatusBarIconController.addIconGroup(mIconManager); - requestApplyInsets(); - } - - @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { // Handle padding of the clock DisplayCutout cutout = insets.getDisplayCutout(); @@ -552,17 +394,14 @@ public class QuickStatusBarHeader extends RelativeLayout implements if (cutout != null) { Rect topCutout = cutout.getBoundingRectTop(); if (topCutout.isEmpty() || cornerCutout) { - mHasTopCutout = false; lp.width = 0; mSpace.setVisibility(View.GONE); } else { - mHasTopCutout = true; lp.width = topCutout.width(); mSpace.setVisibility(View.VISIBLE); } } mSpace.setLayoutParams(lp); - setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE); mCutOutPaddingLeft = padding.first; mCutOutPaddingRight = padding.second; mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top; @@ -600,102 +439,14 @@ public class QuickStatusBarHeader extends RelativeLayout implements 0); } - @Override - @VisibleForTesting - public void onDetachedFromWindow() { - setListening(false); - mRingerModeTracker.getRingerModeInternal().removeObservers(this); - mStatusBarIconController.removeIconGroup(mIconManager); - super.onDetachedFromWindow(); - } - - public void setListening(boolean listening) { - if (listening == mListening) { - return; - } - mHeaderQsPanel.setListening(listening); - if (mHeaderQsPanel.switchTileLayout()) { - updateResources(); - } - mListening = listening; - - if (listening) { - mZenController.addCallback(this); - mAlarmController.addCallback(this); - mLifecycle.setCurrentState(Lifecycle.State.RESUMED); - // Get the most up to date info - mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); - mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); - mPrivacyItemController.addCallback(mPICCallback); - } else { - mZenController.removeCallback(this); - mAlarmController.removeCallback(this); - mLifecycle.setCurrentState(Lifecycle.State.CREATED); - mPrivacyItemController.removeCallback(mPICCallback); - mPrivacyChipLogged = false; - } - } - - @Override - public void onClick(View v) { - if (v == mClockView) { - mActivityStarter.postStartActivityDismissingKeyguard(new Intent( - AlarmClock.ACTION_SHOW_ALARMS), 0); - } else if (v == mNextAlarmContainer && mNextAlarmContainer.isVisibleToUser()) { - if (mNextAlarm.getShowIntent() != null) { - mActivityStarter.postStartActivityDismissingKeyguard( - mNextAlarm.getShowIntent()); - } else { - Log.d(TAG, "No PendingIntent for next alarm. Using default intent"); - mActivityStarter.postStartActivityDismissingKeyguard(new Intent( - AlarmClock.ACTION_SHOW_ALARMS), 0); - } - } else if (v == mPrivacyChip) { - // If the privacy chip is visible, it means there were some indicators - Handler mUiHandler = new Handler(Looper.getMainLooper()); - mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK); - mUiHandler.post(() -> { - mActivityStarter.postStartActivityDismissingKeyguard( - new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0); - mHost.collapsePanels(); - }); - } else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) { - mActivityStarter.postStartActivityDismissingKeyguard(new Intent( - Settings.ACTION_SOUND_SETTINGS), 0); - } - } - - @Override - public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { - mNextAlarm = nextAlarm; - updateStatusText(); - } - - @Override - public void onZenChanged(int zen) { - updateStatusText(); - } - - @Override - public void onConfigChanged(ZenModeConfig config) { - updateStatusText(); - } - public void updateEverything() { post(() -> setClickable(!mExpanded)); } public void setQSPanel(final QSPanel qsPanel) { - mQsPanel = qsPanel; - setupHost(qsPanel.getHost()); - } - - public void setupHost(final QSTileHost host) { - mHost = host; //host.setHeaderView(mExpandIndicator); - mHeaderQsPanel.setQSPanelAndHeader(mQsPanel, this); - mHeaderQsPanel.setHost(host, null /* No customization in header */); - + mHeaderQsPanel.setQSPanelAndHeader(qsPanel, this); + mHeaderQsPanel.setHost(qsPanel.getHost(), null /* No customization in header */); Rect tintArea = new Rect(0, 0, 0, 0); int colorForeground = Utils.getColorAttrDefaultColor(getContext(), @@ -709,12 +460,11 @@ public class QuickStatusBarHeader extends RelativeLayout implements mHeaderQsPanel.setCallback(qsPanelCallback); } - private String formatNextAlarm(AlarmManager.AlarmClockInfo info) { + private String formatNextAlarm(AlarmClockInfo info, boolean use24HourFormat) { if (info == null) { return ""; } - String skeleton = android.text.format.DateFormat - .is24HourFormat(mContext, ActivityManager.getCurrentUser()) ? "EHm" : "Ehma"; + String skeleton = use24HourFormat ? "EHm" : "Ehma"; String pattern = android.text.format.DateFormat .getBestDateTimePattern(Locale.getDefault(), skeleton); return android.text.format.DateFormat.format(pattern, info.getTriggerTime()).toString(); @@ -765,8 +515,4 @@ public class QuickStatusBarHeader extends RelativeLayout implements updateHeaderTextContainerAlphaAnimator(); } } - - private boolean getChipEnabled() { - return mMicCameraIndicatorsEnabled || mAllIndicatorsEnabled; - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java index d899acbade4a..676a300b0ff2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java @@ -16,36 +16,393 @@ package com.android.systemui.qs; +import android.app.AlarmManager.AlarmClockInfo; +import android.content.Intent; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.provider.AlarmClock; +import android.provider.Settings; +import android.service.notification.ZenModeConfig; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; + +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; + +import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; +import com.android.systemui.demomode.DemoMode; +import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.privacy.OngoingPrivacyChip; +import com.android.systemui.privacy.PrivacyChipEvent; +import com.android.systemui.privacy.PrivacyItem; +import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.qs.carrier.QSCarrierGroupController; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.phone.StatusBarIconController; +import com.android.systemui.statusbar.phone.StatusIconContainer; +import com.android.systemui.statusbar.policy.Clock; +import com.android.systemui.statusbar.policy.NextAlarmController; +import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback; +import com.android.systemui.statusbar.policy.ZenModeController; +import com.android.systemui.statusbar.policy.ZenModeController.Callback; +import com.android.systemui.util.RingerModeTracker; +import com.android.systemui.util.ViewController; + +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; -public class QuickStatusBarHeaderController { - private final QuickStatusBarHeader mView; +/** + * Controller for {@link QuickStatusBarHeader}. + */ +class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader> { + private static final String TAG = "QuickStatusBarHeader"; + + private final ZenModeController mZenModeController; + private final NextAlarmController mNextAlarmController; + private final PrivacyItemController mPrivacyItemController; + private final RingerModeTracker mRingerModeTracker; + private final ActivityStarter mActivityStarter; + private final UiEventLogger mUiEventLogger; private final QSCarrierGroupController mQSCarrierGroupController; + private final QuickQSPanel mHeaderQsPanel; + private final LifecycleRegistry mLifecycle; + private final OngoingPrivacyChip mPrivacyChip; + private final Clock mClockView; + private final View mNextAlarmContainer; + private final View mRingerContainer; + private final QSTileHost mQSTileHost; + private final StatusBarIconController mStatusBarIconController; + private final CommandQueue mCommandQueue; + private final DemoModeController mDemoModeController; + private final UserTracker mUserTracker; + private final StatusIconContainer mIconContainer; + private final StatusBarIconController.TintedIconManager mIconManager; + private final DemoMode mDemoModeReceiver; + + private boolean mListening; + private AlarmClockInfo mNextAlarm; + private boolean mAllIndicatorsEnabled; + private boolean mMicCameraIndicatorsEnabled; + private boolean mPrivacyChipLogged; + private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; + + private final ZenModeController.Callback mZenModeControllerCallback = new Callback() { + @Override + public void onZenChanged(int zen) { + mView.updateStatusText(mRingerMode, mNextAlarm, isZenOverridingRinger(), + use24HourFormat()); + } + + @Override + public void onConfigChanged(ZenModeConfig config) { + mView.updateStatusText(mRingerMode, mNextAlarm, isZenOverridingRinger(), + use24HourFormat()); + } + }; + + private boolean use24HourFormat() { + return android.text.format.DateFormat.is24HourFormat( + mView.getContext(), mUserTracker.getUserId()); + + } + + private final NextAlarmChangeCallback mNextAlarmChangeCallback = new NextAlarmChangeCallback() { + @Override + public void onNextAlarmChanged(AlarmClockInfo nextAlarm) { + mNextAlarm = nextAlarm; + mView.updateStatusText(mRingerMode, mNextAlarm, isZenOverridingRinger(), + use24HourFormat()); + } + }; + + private final LifecycleOwner mLifecycleOwner = new LifecycleOwner() { + @NonNull + @Override + public Lifecycle getLifecycle() { + return mLifecycle; + } + }; + + private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() { + @Override + public void onPrivacyItemsChanged(@NonNull List<PrivacyItem> privacyItems) { + mPrivacyChip.setPrivacyList(privacyItems); + setChipVisibility(!privacyItems.isEmpty()); + } + + @Override + public void onFlagAllChanged(boolean flag) { + if (mAllIndicatorsEnabled != flag) { + mAllIndicatorsEnabled = flag; + update(); + } + } + + @Override + public void onFlagMicCameraChanged(boolean flag) { + if (mMicCameraIndicatorsEnabled != flag) { + mMicCameraIndicatorsEnabled = flag; + update(); + } + } + + private void update() { + StatusIconContainer iconContainer = mView.requireViewById(R.id.statusIcons); + iconContainer.setIgnoredSlots(getIgnoredIconSlots()); + setChipVisibility(!mPrivacyChip.getPrivacyList().isEmpty()); + } + }; + + private View.OnClickListener mOnClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (v == mClockView) { + mActivityStarter.postStartActivityDismissingKeyguard(new Intent( + AlarmClock.ACTION_SHOW_ALARMS), 0); + } else if (v == mNextAlarmContainer && mNextAlarmContainer.isVisibleToUser()) { + if (mNextAlarm.getShowIntent() != null) { + mActivityStarter.postStartActivityDismissingKeyguard( + mNextAlarm.getShowIntent()); + } else { + Log.d(TAG, "No PendingIntent for next alarm. Using default intent"); + mActivityStarter.postStartActivityDismissingKeyguard(new Intent( + AlarmClock.ACTION_SHOW_ALARMS), 0); + } + } else if (v == mPrivacyChip) { + // If the privacy chip is visible, it means there were some indicators + Handler mUiHandler = new Handler(Looper.getMainLooper()); + mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK); + mUiHandler.post(() -> { + mActivityStarter.postStartActivityDismissingKeyguard( + new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0); + mQSTileHost.collapsePanels(); + }); + } else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) { + mActivityStarter.postStartActivityDismissingKeyguard(new Intent( + Settings.ACTION_SOUND_SETTINGS), 0); + } + } + }; private QuickStatusBarHeaderController(QuickStatusBarHeader view, + ZenModeController zenModeController, NextAlarmController nextAlarmController, + PrivacyItemController privacyItemController, RingerModeTracker ringerModeTracker, + ActivityStarter activityStarter, UiEventLogger uiEventLogger, + QSTileHost qsTileHost, StatusBarIconController statusBarIconController, + CommandQueue commandQueue, DemoModeController demoModeController, + UserTracker userTracker, QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder) { - mView = view; + super(view); + mZenModeController = zenModeController; + mNextAlarmController = nextAlarmController; + mPrivacyItemController = privacyItemController; + mRingerModeTracker = ringerModeTracker; + mActivityStarter = activityStarter; + mUiEventLogger = uiEventLogger; + mQSTileHost = qsTileHost; + mStatusBarIconController = statusBarIconController; + mCommandQueue = commandQueue; + mDemoModeController = demoModeController; + mUserTracker = userTracker; + mLifecycle = new LifecycleRegistry(mLifecycleOwner); + mQSCarrierGroupController = qsCarrierGroupControllerBuilder .setQSCarrierGroup(mView.findViewById(R.id.carrier_group)) .build(); + + + mPrivacyChip = mView.findViewById(R.id.privacy_chip); + mHeaderQsPanel = mView.findViewById(R.id.quick_qs_panel); + mNextAlarmContainer = mView.findViewById(R.id.alarm_container); + mClockView = mView.findViewById(R.id.clock); + mRingerContainer = mView.findViewById(R.id.ringer_container); + mIconContainer = mView.findViewById(R.id.statusIcons); + + mIconManager = new StatusBarIconController.TintedIconManager(mIconContainer, mCommandQueue); + mDemoModeReceiver = new ClockDemoModeReceiver(mClockView); + } + + @Override + protected void onViewAttached() { + mRingerModeTracker.getRingerModeInternal().observe(mLifecycleOwner, ringer -> { + mRingerMode = ringer; + mView.updateStatusText(mRingerMode, mNextAlarm, isZenOverridingRinger(), + use24HourFormat()); + }); + + mClockView.setOnClickListener(mOnClickListener); + mNextAlarmContainer.setOnClickListener(mOnClickListener); + mRingerContainer.setOnClickListener(mOnClickListener); + mPrivacyChip.setOnClickListener(mOnClickListener); + + // Ignore privacy icons because they show in the space above QQS + mIconContainer.addIgnoredSlots(getIgnoredIconSlots()); + mIconContainer.setShouldRestrictIcons(false); + mStatusBarIconController.addIconGroup(mIconManager); + + mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); + mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); + + setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE); + + mView.onAttach(mIconManager); + + mDemoModeController.addCallback(mDemoModeReceiver); + } + + @Override + protected void onViewDetached() { + mRingerModeTracker.getRingerModeInternal().removeObservers(mLifecycleOwner); + mClockView.setOnClickListener(null); + mNextAlarmContainer.setOnClickListener(null); + mRingerContainer.setOnClickListener(null); + mPrivacyChip.setOnClickListener(null); + mStatusBarIconController.removeIconGroup(mIconManager); + mDemoModeController.removeCallback(mDemoModeReceiver); + setListening(false); } public void setListening(boolean listening) { mQSCarrierGroupController.setListening(listening); - // TODO: move mView.setListening logic into here. - mView.setListening(listening); + + if (listening == mListening) { + return; + } + mListening = listening; + + mHeaderQsPanel.setListening(listening); + if (mHeaderQsPanel.switchTileLayout()) { + mView.updateResources(); + } + + if (listening) { + mZenModeController.addCallback(mZenModeControllerCallback); + mNextAlarmController.addCallback(mNextAlarmChangeCallback); + mLifecycle.setCurrentState(Lifecycle.State.RESUMED); + // Get the most up to date info + mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); + mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); + mPrivacyItemController.addCallback(mPICCallback); + } else { + mZenModeController.removeCallback(mZenModeControllerCallback); + mNextAlarmController.removeCallback(mNextAlarmChangeCallback); + mLifecycle.setCurrentState(Lifecycle.State.CREATED); + mPrivacyItemController.removeCallback(mPICCallback); + mPrivacyChipLogged = false; + } } + private void setChipVisibility(boolean chipVisible) { + if (chipVisible && getChipEnabled()) { + mPrivacyChip.setVisibility(View.VISIBLE); + // Makes sure that the chip is logged as viewed at most once each time QS is opened + // mListening makes sure that the callback didn't return after the user closed QS + if (!mPrivacyChipLogged && mListening) { + mPrivacyChipLogged = true; + mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW); + } + } else { + mPrivacyChip.setVisibility(View.GONE); + } + } - public static class Builder { + private List<String> getIgnoredIconSlots() { + ArrayList<String> ignored = new ArrayList<>(); + if (getChipEnabled()) { + ignored.add(mView.getResources().getString( + com.android.internal.R.string.status_bar_camera)); + ignored.add(mView.getResources().getString( + com.android.internal.R.string.status_bar_microphone)); + if (mAllIndicatorsEnabled) { + ignored.add(mView.getResources().getString( + com.android.internal.R.string.status_bar_location)); + } + } + + return ignored; + } + + private boolean getChipEnabled() { + return mMicCameraIndicatorsEnabled || mAllIndicatorsEnabled; + } + + private boolean isZenOverridingRinger() { + return ZenModeConfig.isZenOverridingRinger(mZenModeController.getZen(), + mZenModeController.getConsolidatedPolicy()); + } + + + private static class ClockDemoModeReceiver implements DemoMode { + private Clock mClockView; + + @Override + public List<String> demoCommands() { + return List.of(COMMAND_CLOCK); + } + + ClockDemoModeReceiver(Clock clockView) { + mClockView = clockView; + } + + @Override + public void dispatchDemoCommand(String command, Bundle args) { + mClockView.dispatchDemoCommand(command, args); + } + + @Override + public void onDemoModeStarted() { + mClockView.onDemoModeStarted(); + } + + @Override + public void onDemoModeFinished() { + mClockView.onDemoModeFinished(); + } + } + + static class Builder { + private final ZenModeController mZenModeController; + private final NextAlarmController mNextAlarmController; + private final PrivacyItemController mPrivacyItemController; + private final RingerModeTracker mRingerModeTracker; + private final ActivityStarter mActivityStarter; + private final UiEventLogger mUiEventLogger; + private final QSTileHost mQsTileHost; + private final StatusBarIconController mStatusBarIconController; + private final CommandQueue mCommandQueue; + private final DemoModeController mDemoModeController; + private final UserTracker mUserTracker; private final QSCarrierGroupController.Builder mQSCarrierGroupControllerBuilder; private QuickStatusBarHeader mView; @Inject - public Builder(QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder) { + Builder(ZenModeController zenModeController, NextAlarmController nextAlarmController, + PrivacyItemController privacyItemController, RingerModeTracker ringerModeTracker, + ActivityStarter activityStarter, UiEventLogger uiEventLogger, QSTileHost qsTileHost, + StatusBarIconController statusBarIconController, CommandQueue commandQueue, + DemoModeController demoModeController, UserTracker userTracker, + QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder) { + mZenModeController = zenModeController; + mNextAlarmController = nextAlarmController; + mPrivacyItemController = privacyItemController; + mRingerModeTracker = ringerModeTracker; + mActivityStarter = activityStarter; + mUiEventLogger = uiEventLogger; + mQsTileHost = qsTileHost; + mStatusBarIconController = statusBarIconController; + mCommandQueue = commandQueue; + mDemoModeController = demoModeController; + mUserTracker = userTracker; mQSCarrierGroupControllerBuilder = qsCarrierGroupControllerBuilder; } @@ -54,8 +411,13 @@ public class QuickStatusBarHeaderController { return this; } - public QuickStatusBarHeaderController build() { - return new QuickStatusBarHeaderController(mView, mQSCarrierGroupControllerBuilder); + + QuickStatusBarHeaderController build() { + return new QuickStatusBarHeaderController(mView, mZenModeController, + mNextAlarmController, mPrivacyItemController, mRingerModeTracker, + mActivityStarter, mUiEventLogger, mQsTileHost, mStatusBarIconController, + mCommandQueue, mDemoModeController, mUserTracker, + mQSCarrierGroupControllerBuilder); } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java b/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java index 65d815053e47..3ee3e117fb0f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java +++ b/packages/SystemUI/src/com/android/systemui/qs/SecureSetting.java @@ -16,7 +16,6 @@ package com.android.systemui.qs; -import android.app.ActivityManager; import android.content.Context; import android.database.ContentObserver; import android.os.Handler; @@ -37,10 +36,6 @@ public abstract class SecureSetting extends ContentObserver implements Listenabl protected abstract void handleValueChanged(int value, boolean observedChange); - protected SecureSetting(Context context, Handler handler, String settingName) { - this(context, handler, settingName, ActivityManager.getCurrentUser()); - } - public SecureSetting(Context context, Handler handler, String settingName, int userId) { super(handler); mContext = context; diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index e5ed88c10a2e..55b67e061c13 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -132,6 +132,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene layout.setSpanSizeLookup(mTileAdapter.getSizeLookup()); mRecyclerView.setLayoutManager(layout); mRecyclerView.addItemDecoration(mTileAdapter.getItemDecoration()); + mRecyclerView.addItemDecoration(mTileAdapter.getMarginItemDecoration()); DefaultItemAnimator animator = new DefaultItemAnimator(); animator.setMoveDuration(TileAdapter.MOVE_DURATION); mRecyclerView.setItemAnimator(animator); @@ -221,6 +222,22 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene } } + /** + * Sets the padding for the RecyclerView. Also, updates the margin between the tiles in the + * {@link TileAdapter}. + */ + public void setContentPaddings(int paddingStart, int paddingEnd) { + int halfMargin = mContext.getResources() + .getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal) / 2; + mTileAdapter.changeHalfMargin(halfMargin); + mRecyclerView.setPaddingRelative( + paddingStart, + mRecyclerView.getPaddingTop(), + paddingEnd, + mRecyclerView.getPaddingBottom() + ); + } + private void queryTiles() { mTileQueryHelper.queryTiles(mHost); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java index bffeb3ec3c70..b471dfae02d1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -18,6 +18,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Handler; import android.view.LayoutInflater; @@ -75,6 +76,7 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta private final List<TileInfo> mTiles = new ArrayList<>(); private final ItemTouchHelper mItemTouchHelper; private final ItemDecoration mDecoration; + private final MarginTileDecoration mMarginDecoration; private final int mMinNumTiles; private int mEditIndex; private int mTileDividerIndex; @@ -97,6 +99,7 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta mUiEventLogger = uiEventLogger; mItemTouchHelper = new ItemTouchHelper(mCallbacks); mDecoration = new TileItemDecoration(context); + mMarginDecoration = new MarginTileDecoration(); mMinNumTiles = context.getResources().getInteger(R.integer.quick_settings_min_num_tiles); mAccessibilityDelegate = new TileAdapterDelegate(); } @@ -123,6 +126,14 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta return mDecoration; } + public ItemDecoration getMarginItemDecoration() { + return mMarginDecoration; + } + + public void changeHalfMargin(int halfMargin) { + mMarginDecoration.setHalfMargin(halfMargin); + } + public void saveSpecs(QSTileHost host) { List<String> newSpecs = new ArrayList<>(); clearAccessibilityState(); @@ -596,7 +607,6 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta mDrawable = context.getDrawable(R.drawable.qs_customize_tile_decoration); } - @Override public void onDraw(Canvas c, RecyclerView parent, State state) { super.onDraw(c, parent, state); @@ -607,6 +617,12 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final ViewHolder holder = parent.getChildViewHolder(child); + // Do not draw background for the holder that's currently being dragged + if (holder == mCurrentDrag) { + continue; + } + // Do not draw background for holders before the edit index (header and current + // tiles) if (holder.getAdapterPosition() == 0 || holder.getAdapterPosition() < mEditIndex && !(child instanceof TextView)) { continue; @@ -624,6 +640,25 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta } } + private static class MarginTileDecoration extends ItemDecoration { + private int mHalfMargin; + + public void setHalfMargin(int halfMargin) { + mHalfMargin = halfMargin; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, + @NonNull RecyclerView parent, @NonNull State state) { + if (view instanceof TextView) { + super.getItemOffsets(outRect, view, parent, state); + } else { + outRect.left = mHalfMargin; + outRect.right = mHalfMargin; + } + } + } + private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index 73c6504b9983..b795a5f5ea19 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -17,7 +17,6 @@ package com.android.systemui.qs.customize; import android.Manifest.permission; -import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -40,6 +39,7 @@ import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.QSTileHost; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon; +import com.android.systemui.settings.UserTracker; import com.android.systemui.util.leak.GarbageMonitor; import java.util.ArrayList; @@ -58,16 +58,18 @@ public class TileQueryHelper { private final Executor mMainExecutor; private final Executor mBgExecutor; private final Context mContext; + private final UserTracker mUserTracker; private TileStateListener mListener; private boolean mFinished; @Inject - public TileQueryHelper(Context context, + public TileQueryHelper(Context context, UserTracker userTracker, @Main Executor mainExecutor, @Background Executor bgExecutor) { mContext = context; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; + mUserTracker = userTracker; } public void setListener(TileStateListener listener) { @@ -207,7 +209,7 @@ public class TileQueryHelper { Collection<QSTile> params = host.getTiles(); PackageManager pm = mContext.getPackageManager(); List<ResolveInfo> services = pm.queryIntentServicesAsUser( - new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser()); + new Intent(TileService.ACTION_QS_TILE), 0, mUserTracker.getUserId()); String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock); for (ResolveInfo info : services) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index 19c7b6cefc5d..6e28cd89d43a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -18,7 +18,6 @@ package com.android.systemui.qs.external; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG; -import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -304,8 +303,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener } private Intent resolveIntent(Intent i) { - ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0, - ActivityManager.getCurrentUser()); + ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0, mUser); return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES) .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java index cfa8fb6373a1..7e76e57f4802 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java @@ -15,7 +15,6 @@ */ package com.android.systemui.qs.external; -import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -26,7 +25,6 @@ import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Handler; import android.os.IBinder; -import android.os.UserHandle; import android.service.quicksettings.IQSTileService; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; @@ -36,6 +34,7 @@ import androidx.annotation.VisibleForTesting; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener; +import com.android.systemui.settings.UserTracker; import java.util.List; import java.util.Objects; @@ -60,6 +59,7 @@ public class TileServiceManager { private final TileServices mServices; private final TileLifecycleManager mStateManager; private final Handler mHandler; + private final UserTracker mUserTracker; private boolean mBindRequested; private boolean mBindAllowed; private boolean mBound; @@ -73,25 +73,26 @@ public class TileServiceManager { private boolean mStarted = false; TileServiceManager(TileServices tileServices, Handler handler, ComponentName component, - Tile tile, BroadcastDispatcher broadcastDispatcher) { - this(tileServices, handler, new TileLifecycleManager(handler, + Tile tile, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker) { + this(tileServices, handler, userTracker, new TileLifecycleManager(handler, tileServices.getContext(), tileServices, tile, new Intent().setComponent(component), - new UserHandle(ActivityManager.getCurrentUser()), broadcastDispatcher)); + userTracker.getUserHandle(), broadcastDispatcher)); } @VisibleForTesting - TileServiceManager(TileServices tileServices, Handler handler, + TileServiceManager(TileServices tileServices, Handler handler, UserTracker userTracker, TileLifecycleManager tileLifecycleManager) { mServices = tileServices; mHandler = handler; mStateManager = tileLifecycleManager; + mUserTracker = userTracker; IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addDataScheme("package"); Context context = mServices.getContext(); - context.registerReceiverAsUser(mUninstallReceiver, - new UserHandle(ActivityManager.getCurrentUser()), filter, null, mHandler); + context.registerReceiverAsUser(mUninstallReceiver, userTracker.getUserHandle(), filter, + null, mHandler); } boolean isLifecycleStarted() { @@ -279,7 +280,7 @@ public class TileServiceManager { queryIntent.setPackage(pkgName); PackageManager pm = context.getPackageManager(); List<ResolveInfo> services = pm.queryIntentServicesAsUser( - queryIntent, 0, ActivityManager.getCurrentUser()); + queryIntent, 0, mUserTracker.getUserId()); for (ResolveInfo info : services) { if (Objects.equals(info.serviceInfo.packageName, component.getPackageName()) && Objects.equals(info.serviceInfo.name, component.getClassName())) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index 2863d08a75dc..35cf2a12745e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -39,6 +39,7 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.Dependency; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.qs.QSTileHost; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -62,15 +63,18 @@ public class TileServices extends IQSService.Stub { private final Handler mMainHandler; private final QSTileHost mHost; private final BroadcastDispatcher mBroadcastDispatcher; + private final UserTracker mUserTracker; private int mMaxBound = DEFAULT_MAX_BOUND; - public TileServices(QSTileHost host, Looper looper, BroadcastDispatcher broadcastDispatcher) { + public TileServices(QSTileHost host, Looper looper, BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker) { mHost = host; mContext = mHost.getContext(); mBroadcastDispatcher = broadcastDispatcher; mHandler = new Handler(looper); mMainHandler = new Handler(Looper.getMainLooper()); + mUserTracker = userTracker; mBroadcastDispatcher.registerReceiver( mRequestListeningReceiver, new IntentFilter(TileService.ACTION_REQUEST_LISTENING), @@ -104,7 +108,7 @@ public class TileServices extends IQSService.Stub { protected TileServiceManager onCreateTileService(ComponentName component, Tile tile, BroadcastDispatcher broadcastDispatcher) { return new TileServiceManager(this, mHandler, component, tile, - broadcastDispatcher); + broadcastDispatcher, mUserTracker); } public void freeService(CustomTile tile, TileServiceManager service) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index dfd7e2c8fdb7..5a81676567a7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -31,7 +31,6 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.annotation.CallSuper; import android.annotation.NonNull; -import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -521,9 +520,9 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) { EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, - userRestriction, ActivityManager.getCurrentUser()); + userRestriction, mHost.getUserId()); if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, - userRestriction, ActivityManager.getCurrentUser())) { + userRestriction, mHost.getUserId())) { state.disabledByPolicy = true; mEnforcedAdmin = admin; } else { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index 3495d3065fe9..becbfd57c14b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -52,6 +52,7 @@ import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.CastController.CastDevice; +import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.NetworkController; @@ -73,6 +74,7 @@ public class CastTile extends QSTileImpl<BooleanState> { private final Callback mCallback = new Callback(); private Dialog mDialog; private boolean mWifiConnected; + private boolean mHotspotConnected; private static final String WFD_ENABLE = "persist.debug.wfd.enable"; @Inject @@ -86,7 +88,8 @@ public class CastTile extends QSTileImpl<BooleanState> { QSLogger qsLogger, CastController castController, KeyguardStateController keyguardStateController, - NetworkController networkController + NetworkController networkController, + HotspotController hotspotController ) { super(host, backgroundLooper, mainHandler, metricsLogger, statusBarStateController, activityStarter, qsLogger); @@ -97,6 +100,7 @@ public class CastTile extends QSTileImpl<BooleanState> { mController.observe(this, mCallback); mKeyguard.observe(this, mCallback); mNetworkController.observe(this, mSignalCallback); + hotspotController.observe(this, mHotspotCallback); } @Override @@ -224,7 +228,7 @@ public class CastTile extends QSTileImpl<BooleanState> { } state.icon = ResourceIcon.get(state.value ? R.drawable.ic_cast_connected : R.drawable.ic_cast); - if (mWifiConnected || state.value) { + if (canCastToWifi() || state.value) { state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; if (!state.value) { state.secondaryLabel = ""; @@ -260,6 +264,10 @@ public class CastTile extends QSTileImpl<BooleanState> { : mContext.getString(R.string.quick_settings_cast_device_default_name); } + private boolean canCastToWifi() { + return mWifiConnected || mHotspotConnected; + } + private final NetworkController.SignalCallback mSignalCallback = new NetworkController.SignalCallback() { @Override @@ -277,6 +285,24 @@ public class CastTile extends QSTileImpl<BooleanState> { boolean enabledAndConnected = enabled && qsIcon.visible; if (enabledAndConnected != mWifiConnected) { mWifiConnected = enabledAndConnected; + // Hotspot is not connected, so changes here should update + if (!mHotspotConnected) { + refreshState(); + } + } + } + } + }; + + private final HotspotController.Callback mHotspotCallback = + new HotspotController.Callback() { + @Override + public void onHotspotChanged(boolean enabled, int numDevices) { + boolean enabledAndConnected = enabled && numDevices > 0; + if (enabledAndConnected != mHotspotConnected) { + mHotspotConnected = enabledAndConnected; + // Wifi is not connected, so changes here should update + if (!mWifiConnected) { refreshState(); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java index 347ef45824c9..98782f7c8b55 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java @@ -38,6 +38,7 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.qs.SecureSetting; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.settings.UserTracker; import javax.inject.Inject; @@ -61,13 +62,14 @@ public class ColorInversionTile extends QSTileImpl<BooleanState> { MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, - QSLogger qsLogger + QSLogger qsLogger, + UserTracker userTracker ) { super(host, backgroundLooper, mainHandler, metricsLogger, statusBarStateController, activityStarter, qsLogger); mSetting = new SecureSetting(mContext, mainHandler, - Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) { + Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, userTracker.getUserId()) { @Override protected void handleValueChanged(int value, boolean observedChange) { // mHandler is the background handler so calling this is OK diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java index ec8b1435e201..2076cbffa425 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java @@ -19,7 +19,6 @@ package com.android.systemui.qs.tiles; import static android.provider.Settings.Global.ZEN_MODE_ALARMS; import static android.provider.Settings.Global.ZEN_MODE_OFF; -import android.app.ActivityManager; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.Context; @@ -203,7 +202,7 @@ public class DndTile extends QSTileImpl<BooleanState> { break; default: Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration, - ActivityManager.getCurrentUser(), true).id; + mHost.getUserId(), true).id; mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionId, TAG); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 201ed9c9ebec..8ddd4c9816cd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -155,7 +155,7 @@ public class UserDetailView extends PseudoGridView { } view.setActivated(true); } - switchTo(tag); + onUserListItemClicked(tag); } } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java index 47002683c6b9..bbeff6ece902 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyRecentsImpl.java @@ -16,30 +16,17 @@ package com.android.systemui.recents; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; - import android.annotation.Nullable; -import android.app.ActivityManager; import android.app.trust.TrustManager; import android.content.Context; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.RemoteException; import android.util.Log; -import android.view.Display; -import android.widget.Toast; import com.android.systemui.Dependency; -import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.shared.recents.IOverviewProxy; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.statusbar.phone.StatusBar; -import com.android.wm.shell.splitscreen.SplitScreen; import java.util.Optional; @@ -56,7 +43,6 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { private final static String TAG = "OverviewProxyRecentsImpl"; @Nullable private final Lazy<StatusBar> mStatusBarLazy; - private final Optional<SplitScreen> mSplitScreenOptional; private Context mContext; private Handler mHandler; @@ -65,10 +51,8 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Inject - public OverviewProxyRecentsImpl(Optional<Lazy<StatusBar>> statusBarLazy, - Optional<SplitScreen> splitScreenOptional) { + public OverviewProxyRecentsImpl(Optional<Lazy<StatusBar>> statusBarLazy) { mStatusBarLazy = statusBarLazy.orElse(null); - mSplitScreenOptional = splitScreenOptional; } @Override @@ -140,42 +124,4 @@ public class OverviewProxyRecentsImpl implements RecentsImplementation { // Do nothing } } - - @Override - public boolean splitPrimaryTask(int stackCreateMode, Rect initialBounds, - int metricsDockAction) { - Point realSize = new Point(); - if (initialBounds == null) { - mContext.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY) - .getRealSize(realSize); - initialBounds = new Rect(0, 0, realSize.x, realSize.y); - } - - ActivityManager.RunningTaskInfo runningTask = - ActivityManagerWrapper.getInstance().getRunningTask(); - final int activityType = runningTask != null - ? runningTask.configuration.windowConfiguration.getActivityType() - : ACTIVITY_TYPE_UNDEFINED; - boolean screenPinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive(); - boolean isRunningTaskInHomeOrRecentsStack = - activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS; - if (runningTask != null && !isRunningTaskInHomeOrRecentsStack && !screenPinningActive) { - if (runningTask.supportsSplitScreenMultiWindow) { - if (ActivityManagerWrapper.getInstance().setTaskWindowingModeSplitScreenPrimary( - runningTask.id, stackCreateMode, initialBounds)) { - mSplitScreenOptional.ifPresent(splitScreen -> { - splitScreen.onDockedTopTask(); - // The overview service is handling split screen, so just skip the wait - // for the first draw and notify the divider to start animating now - splitScreen.onRecentsDrawn(); - }); - return true; - } - } else { - Toast.makeText(mContext, R.string.dock_non_resizeble_failed_to_dock_text, - Toast.LENGTH_SHORT).show(); - } - } - return false; - } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 2a976f546ba4..5279a20a67a7 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -16,7 +16,6 @@ package com.android.systemui.recents; -import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; @@ -36,12 +35,14 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_T import android.annotation.FloatRange; import android.app.ActivityTaskManager; +import android.app.PictureInPictureParams; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; import android.graphics.Bitmap; import android.graphics.Insets; import android.graphics.Rect; @@ -65,6 +66,7 @@ import android.view.accessibility.AccessibilityManager; import androidx.annotation.NonNull; import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.util.ScreenshotHelper; import com.android.systemui.Dumpable; @@ -75,8 +77,6 @@ import com.android.systemui.navigationbar.NavigationBar; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; -import com.android.systemui.pip.Pip; -import com.android.systemui.pip.PipAnimationController; import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.shared.recents.IOverviewProxy; @@ -93,6 +93,9 @@ import com.android.systemui.statusbar.phone.StatusBarWindowCallback; import com.android.systemui.statusbar.policy.CallbackController; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEvents; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.phone.PipUtils; import com.android.wm.shell.splitscreen.SplitScreen; import java.io.FileDescriptor; @@ -101,11 +104,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.BiConsumer; +import java.util.function.Consumer; import javax.inject.Inject; import dagger.Lazy; + /** * Class to send information from overview to launcher with a binder. */ @@ -141,6 +146,7 @@ public class OverviewProxyService extends CurrentUserTracker implements private Region mActiveNavBarRegion; + private IPinnedStackAnimationListener mIPinnedStackAnimationListener; private IOverviewProxy mOverviewProxy; private int mConnectionBackoffAttempts; private boolean mBound; @@ -155,14 +161,14 @@ public class OverviewProxyService extends CurrentUserTracker implements private boolean mSupportsRoundedCornersOnWindows; private int mNavBarMode = NAV_BAR_MODE_3BUTTON; - private ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() { - + @VisibleForTesting + public ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() { @Override public void startScreenPinning(int taskId) { if (!verifyCaller("startScreenPinning")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> { mStatusBarOptionalLazy.ifPresent( @@ -179,7 +185,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("stopScreenPinning")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> { try { @@ -199,7 +205,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("onStatusBarMotionEvent")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { // TODO move this logic to message queue mStatusBarOptionalLazy.ifPresent(statusBarLazy -> { @@ -230,26 +236,11 @@ public class OverviewProxyService extends CurrentUserTracker implements } @Override - public void onSplitScreenInvoked() { - if (!verifyCaller("onSplitScreenInvoked")) { - return; - } - long token = Binder.clearCallingIdentity(); - try { - mSplitScreenOptional.ifPresent(splitScreen -> { - splitScreen.onDockedFirstAnimationFrame(); - }); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override public void onOverviewShown(boolean fromHome) { if (!verifyCaller("onOverviewShown")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> { for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { @@ -266,7 +257,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("getNonMinimizedSplitScreenSecondaryBounds")) { return null; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { return mSplitScreenOptional.map(splitScreen -> splitScreen.getDividerView().getNonMinimizedSplitScreenSecondaryBounds()) @@ -281,7 +272,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("setNavBarButtonAlpha")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mNavBarButtonAlpha = alpha; mHandler.post(() -> notifyNavBarButtonAlphaChanged(alpha, animate)); @@ -300,7 +291,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("onAssistantProgress")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> notifyAssistantProgress(progress)); } finally { @@ -313,7 +304,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("onAssistantGestureCompletion")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> notifyAssistantGestureCompletion(velocity)); } finally { @@ -326,7 +317,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("startAssistant")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> notifyStartAssistant(bundle)); } finally { @@ -339,7 +330,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("monitorGestureInput")) { return null; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { final InputMonitor monitor = InputManager.getInstance().monitorGestureInput(name, displayId); @@ -357,7 +348,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("notifyAccessibilityButtonClicked")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { AccessibilityManager.getInstance(mContext) .notifyAccessibilityButtonClicked(displayId); @@ -371,7 +362,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("notifyAccessibilityButtonLongClicked")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); @@ -391,7 +382,7 @@ public class OverviewProxyService extends CurrentUserTracker implements "ByPass setShelfHeight, FEATURE_PICTURE_IN_PICTURE:" + mHasPipFeature); return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mPipOptional.ifPresent( pip -> pip.setShelfHeight(visible, shelfHeight)); @@ -419,7 +410,7 @@ public class OverviewProxyService extends CurrentUserTracker implements + mHasPipFeature); return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mPipOptional.ifPresent( pip -> pip.setPinnedStackAnimationType( @@ -436,10 +427,11 @@ public class OverviewProxyService extends CurrentUserTracker implements + mHasPipFeature); return; } - long token = Binder.clearCallingIdentity(); + mIPinnedStackAnimationListener = listener; + final long token = Binder.clearCallingIdentity(); try { mPipOptional.ifPresent( - pip -> pip.setPinnedStackAnimationListener(listener)); + pip -> pip.setPinnedStackAnimationListener(mPinnedStackAnimationCallback)); } finally { Binder.restoreCallingIdentity(token); } @@ -450,7 +442,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("onQuickSwitchToNewTask")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mHandler.post(() -> notifyQuickSwitchToNewTask(rotation)); } finally { @@ -463,7 +455,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("startOneHandedMode")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mOneHandedOptional.ifPresent(oneHanded -> oneHanded.startOneHanded()); } finally { @@ -476,7 +468,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("stopOneHandedMode")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mOneHandedOptional.ifPresent(oneHanded -> oneHanded.stopOneHanded( OneHandedEvents.EVENT_ONE_HANDED_TRIGGER_GESTURE_OUT)); @@ -505,7 +497,7 @@ public class OverviewProxyService extends CurrentUserTracker implements if (!verifyCaller("expandNotificationPanel")) { return; } - long token = Binder.clearCallingIdentity(); + final long token = Binder.clearCallingIdentity(); try { mCommandQueue.handleSystemKey(KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN); } finally { @@ -513,6 +505,38 @@ public class OverviewProxyService extends CurrentUserTracker implements } } + @Override + public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, + PictureInPictureParams pictureInPictureParams, + int launcherRotation, int shelfHeight) { + if (!verifyCaller("startSwipePipToHome") || !mHasPipFeature) { + return null; + } + final long binderToken = Binder.clearCallingIdentity(); + try { + return mPipOptional.map(pip -> + pip.startSwipePipToHome(componentName, activityInfo, + pictureInPictureParams, launcherRotation, shelfHeight)) + .orElse(null); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + + @Override + public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds) { + if (!verifyCaller("stopSwipePipToHome") || !mHasPipFeature) { + return; + } + final long binderToken = Binder.clearCallingIdentity(); + try { + mPipOptional.ifPresent(pip -> pip.stopSwipePipToHome( + componentName, destinationBounds)); + } finally { + Binder.restoreCallingIdentity(binderToken); + } + } + private boolean verifyCaller(String reason) { final int callerId = Binder.getCallingUserHandle().getIdentifier(); if (callerId != mCurrentBoundedUserId) { @@ -605,6 +629,8 @@ public class OverviewProxyService extends CurrentUserTracker implements private final StatusBarWindowCallback mStatusBarWindowCallback = this::onStatusBarStateChanged; private final BiConsumer<Rect, Rect> mSplitScreenBoundsChangeListener = this::notifySplitScreenBoundsChanged; + private final Consumer<Boolean> mPinnedStackAnimationCallback = + this::notifyPinnedStackAnimationStarted; // This is the death handler for the binder from the launcher service private final IBinder.DeathRecipient mOverviewServiceDeathRcpt @@ -624,7 +650,7 @@ public class OverviewProxyService extends CurrentUserTracker implements super(broadcastDispatcher); mContext = context; mPipOptional = pipOptional; - mHasPipFeature = mContext.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE); + mHasPipFeature = PipUtils.hasSystemFeature(mContext); mStatusBarOptionalLazy = statusBarOptionalLazy; mHandler = new Handler(); mNavBarControllerLazy = navBarControllerLazy; @@ -734,6 +760,17 @@ public class OverviewProxyService extends CurrentUserTracker implements } } + private void notifyPinnedStackAnimationStarted(Boolean isAnimationStarted) { + if (mIPinnedStackAnimationListener == null) { + return; + } + try { + mIPinnedStackAnimationListener.onPinnedStackAnimationStarted(); + } catch (RemoteException e) { + Log.e(TAG_OPS, "Failed to call onPinnedStackAnimationStarted()", e); + } + } + private void onStatusBarStateChanged(boolean keyguardShowing, boolean keyguardOccluded, boolean bouncerShowing) { mSysUiState.setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING, @@ -764,7 +801,7 @@ public class OverviewProxyService extends CurrentUserTracker implements public void cleanupAfterDeath() { if (mInputFocusTransferStarted) { - mHandler.post(()-> { + mHandler.post(() -> { mStatusBarOptionalLazy.ifPresent(statusBarLazy -> { mInputFocusTransferStarted = false; statusBarLazy.get().onInputFocusTransfer(false, true /* cancel */, diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java index df61fd19ad45..6f6dd9cb22c7 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java @@ -19,7 +19,6 @@ package com.android.systemui.recents; import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; -import android.graphics.Rect; import android.provider.Settings; import com.android.systemui.SystemUI; @@ -120,17 +119,6 @@ public class Recents extends SystemUI implements CommandQueue.Callbacks { mImpl.cancelPreloadRecentApps(); } - public boolean splitPrimaryTask(int stackCreateMode, Rect initialBounds, - int metricsDockAction) { - // Ensure the device has been provisioned before allowing the user to interact with - // recents - if (!isUserSetup()) { - return false; - } - - return mImpl.splitPrimaryTask(stackCreateMode, initialBounds, metricsDockAction); - } - /** * @return whether this device is provisioned and the current user is set up. */ diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java index a641730ac64e..8848dbbda5e7 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImplementation.java @@ -17,7 +17,6 @@ package com.android.systemui.recents; import android.content.Context; import android.content.res.Configuration; -import android.graphics.Rect; import java.io.PrintWriter; @@ -35,10 +34,6 @@ public interface RecentsImplementation { default void showRecentApps(boolean triggeredFromAltTab) {} default void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {} default void toggleRecentApps() {} - default boolean splitPrimaryTask(int stackCreateMode, Rect initialBounds, - int metricsDockAction) { - return false; - } default void dump(PrintWriter pw) {} } diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java index 9c5a3de4523a..ccf2598e4f18 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsOnboarding.java @@ -67,6 +67,7 @@ import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import java.io.PrintWriter; import java.util.Collections; @@ -365,7 +366,7 @@ public class RecentsOnboarding { mOverviewProxyListenerRegistered = true; } if (!mTaskListenerRegistered) { - ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskListener); + TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskListener); mTaskListenerRegistered = true; } } @@ -377,7 +378,7 @@ public class RecentsOnboarding { mOverviewProxyListenerRegistered = false; } if (mTaskListenerRegistered) { - ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskListener); + TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskListener); mTaskListenerRegistered = false; } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java index df03c3e08f08..0aa9d4d662a5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java @@ -48,6 +48,7 @@ public class ScreenInternalAudioRecorder { private long mTotalBytes; private MediaMuxer mMuxer; private boolean mMic; + private boolean mStarted; private int mTrackId = -1; @@ -263,10 +264,14 @@ public class ScreenInternalAudioRecorder { * start recording * @throws IllegalStateException if recording fails to initialize */ - public void start() throws IllegalStateException { - if (mThread != null) { - Log.e(TAG, "a recording is being done in parallel or stop is not called"); + public synchronized void start() throws IllegalStateException { + if (mStarted) { + if (mThread == null) { + throw new IllegalStateException("Recording stopped and can't restart (single use)"); + } + throw new IllegalStateException("Recording already started"); } + mStarted = true; mAudioRecord.startRecording(); if (mMic) mAudioRecordMic.startRecording(); Log.d(TAG, "channel count " + mAudioRecord.getChannelCount()); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 2b4fa2a23a07..aaa335c25d5d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -69,6 +69,7 @@ import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.AnimationUtils; @@ -104,7 +105,6 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset public Bitmap image; public Consumer<Uri> finisher; public GlobalScreenshot.ActionsReadyListener mActionsReadyListener; - public int errorMsgResId; void clearImage() { image = null; @@ -184,6 +184,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset private final WindowManager.LayoutParams mWindowLayoutParams; private final Display mDisplay; private final DisplayMetrics mDisplayMetrics; + private final AccessibilityManager mAccessibilityManager; private View mScreenshotLayout; private ScreenshotSelectorView mScreenshotSelectorView; @@ -242,6 +243,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; mUiEventLogger = uiEventLogger; + mAccessibilityManager = AccessibilityManager.getInstance(mContext); reloadAssets(); Configuration config = mContext.getResources().getConfiguration(); @@ -319,8 +321,17 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset Insets visibleInsets, int taskId, int userId, ComponentName topComponent, Consumer<Uri> finisher, Runnable onComplete) { // TODO: use task Id, userId, topComponent for smart handler - mOnCompleteRunnable = onComplete; + + if (screenshot == null) { + Log.e(TAG, "Got null bitmap from screenshot message"); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_capture_text); + finisher.accept(null); + mOnCompleteRunnable.run(); + return; + } + if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false); } else { @@ -567,12 +578,30 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset .build(); final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = SurfaceControl.captureDisplay(captureArgs); - final Bitmap screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); + Bitmap screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); + + if (screenshot == null) { + Log.e(TAG, "Screenshot bitmap was null"); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_capture_text); + finisher.accept(null); + mOnCompleteRunnable.run(); + return; + } + saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true); } private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, boolean showFlash) { + if (mAccessibilityManager.isEnabled()) { + AccessibilityEvent event = + new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + event.setContentDescription( + mContext.getResources().getString(R.string.screenshot_saving_title)); + mAccessibilityManager.sendAccessibilityEvent(event); + } + if (mScreenshotLayout.isAttachedToWindow()) { // if we didn't already dismiss for another reason if (mDismissAnimation == null || !mDismissAnimation.isRunning()) { @@ -583,14 +612,6 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mScreenBitmap = screenshot; - if (mScreenBitmap == null) { - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_capture_text); - finisher.accept(null); - mOnCompleteRunnable.run(); - return; - } - if (!isUserSetupComplete()) { // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing // and sharing shouldn't be exposed to the user. @@ -632,7 +653,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset if (imageData.uri == null) { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_capture_text); + R.string.screenshot_failed_to_save_text); } else { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); @@ -752,7 +773,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset if (imageData.uri == null) { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_capture_text); + R.string.screenshot_failed_to_save_text); } else { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index df1d78953f46..f0ea597c458d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -217,13 +217,11 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { mParams.mActionsReadyListener.onActionsReady(mImageData); mParams.finisher.accept(mImageData.uri); mParams.image = null; - mParams.errorMsgResId = 0; } catch (Exception e) { // IOException/UnsupportedOperationException may be thrown if external storage is // not mounted Slog.e(TAG, "unable to save screenshot", e); mParams.clearImage(); - mParams.errorMsgResId = R.string.screenshot_failed_to_save_text; mImageData.reset(); mParams.mActionsReadyListener.onActionsReady(mImageData); mParams.finisher.accept(null); diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt index 26d408fe4ab7..c7a8fa22d1af 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt @@ -40,6 +40,11 @@ interface UserTracker : UserContentResolverProvider, UserContextProvider { val userHandle: UserHandle /** + * [UserInfo] for current user + */ + val userInfo: UserInfo + + /** * List of profiles associated with the current user. */ val userProfiles: List<UserInfo> diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 4cc0eeee712c..049685f3dda4 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -82,6 +82,12 @@ class UserTrackerImpl internal constructor( override val userContentResolver: ContentResolver get() = userContext.contentResolver + override val userInfo: UserInfo + get() { + val user = userId + return userProfiles.first { it.id == user } + } + /** * Returns a [List<UserInfo>] of all profiles associated with the current user. * diff --git a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java index b9b4f42a66e1..6202057b9ef1 100644 --- a/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java +++ b/packages/SystemUI/src/com/android/systemui/shortcut/ShortcutKeyDispatcher.java @@ -16,9 +16,6 @@ package com.android.systemui.shortcut; -import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT; -import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT; - import android.content.Context; import android.content.res.Configuration; import android.os.RemoteException; @@ -29,7 +26,6 @@ import android.view.WindowManagerGlobal; import com.android.internal.policy.DividerSnapAlgorithm; import com.android.systemui.SystemUI; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.recents.Recents; import com.android.wm.shell.splitscreen.DividerView; import com.android.wm.shell.splitscreen.SplitScreen; @@ -46,7 +42,6 @@ public class ShortcutKeyDispatcher extends SystemUI private static final String TAG = "ShortcutKeyDispatcher"; private final Optional<SplitScreen> mSplitScreenOptional; - private final Recents mRecents; private ShortcutKeyServiceProxy mShortcutKeyServiceProxy = new ShortcutKeyServiceProxy(this); private IWindowManager mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); @@ -60,11 +55,9 @@ public class ShortcutKeyDispatcher extends SystemUI protected final long SC_DOCK_RIGHT = META_MASK | KeyEvent.KEYCODE_RIGHT_BRACKET; @Inject - public ShortcutKeyDispatcher(Context context, - Optional<SplitScreen> splitScreenOptional, Recents recents) { + public ShortcutKeyDispatcher(Context context, Optional<SplitScreen> splitScreenOptional) { super(context); mSplitScreenOptional = splitScreenOptional; - mRecents = recents; } /** @@ -96,8 +89,7 @@ public class ShortcutKeyDispatcher extends SystemUI } private void handleDockKey(long shortcutCode) { - if (mSplitScreenOptional.isPresent()) { - SplitScreen splitScreen = mSplitScreenOptional.get(); + mSplitScreenOptional.ifPresent(splitScreen -> { if (splitScreen.isDividerVisible()) { // If there is already a docked window, we respond by resizing the docking pane. DividerView dividerView = splitScreen.getDividerView(); @@ -112,12 +104,9 @@ public class ShortcutKeyDispatcher extends SystemUI dividerView.stopDragging(target.position, 0f, false /* avoidDismissStart */, true /* logMetrics */); return; + } else { + splitScreen.splitPrimaryTask(); } - } - - // Split the screen - mRecents.splitPrimaryTask((shortcutCode == SC_DOCK_LEFT) - ? SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT - : SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT, null, -1); + }); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 37ae791bf172..dcee9fa9e648 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -58,12 +58,14 @@ import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.view.AppearanceRegion; import com.android.systemui.statusbar.CommandQueue.Callbacks; +import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.policy.CallbackController; import com.android.systemui.tracing.ProtoTracer; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Arrays; /** * This class takes the functions from IStatusBar that come in on @@ -138,6 +140,8 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< private static final int MSG_SUPPRESS_AMBIENT_DISPLAY = 56 << MSG_SHIFT; private static final int MSG_REQUEST_WINDOW_MAGNIFICATION_CONNECTION = 57 << MSG_SHIFT; private static final int MSG_HANDLE_WINDOW_MANAGER_LOGGING_COMMAND = 58 << MSG_SHIFT; + //TODO(b/169175022) Update name and when feature name is locked. + private static final int MSG_EMERGENCY_ACTION_LAUNCH_GESTURE = 59 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -159,6 +163,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< */ private int mLastUpdatedImeDisplayId = INVALID_DISPLAY; private ProtoTracer mProtoTracer; + private final @Nullable CommandRegistry mRegistry; /** * These methods are called back on the main thread. @@ -255,6 +260,11 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< default void showAssistDisclosure() { } default void startAssist(Bundle args) { } default void onCameraLaunchGestureDetected(int source) { } + + /** + * Notifies SysUI that the emergency action gesture was detected. + */ + default void onEmergencyActionLaunchGestureDetected() { } default void showPictureInPictureMenu() { } default void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) { } @@ -368,11 +378,12 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } public CommandQueue(Context context) { - this(context, null); + this(context, null, null); } - public CommandQueue(Context context, ProtoTracer protoTracer) { + public CommandQueue(Context context, ProtoTracer protoTracer, CommandRegistry registry) { mProtoTracer = protoTracer; + mRegistry = registry; context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler); // We always have default display. setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE); @@ -726,6 +737,14 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } @Override + public void onEmergencyActionLaunchGestureDetected() { + synchronized (mLock) { + mHandler.removeMessages(MSG_EMERGENCY_ACTION_LAUNCH_GESTURE); + mHandler.obtainMessage(MSG_EMERGENCY_ACTION_LAUNCH_GESTURE).sendToTarget(); + } + } + + @Override public void addQsTile(ComponentName tile) { synchronized (mLock) { mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget(); @@ -1013,6 +1032,34 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } } + @Override + public void passThroughShellCommand(String[] args, ParcelFileDescriptor pfd) { + final FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor()); + final PrintWriter pw = new PrintWriter(fos); + // This is mimicking Binder#dumpAsync, but on this side of the binder. Might be possible + // to just throw this work onto the handler just like the other messages + Thread thr = new Thread("Sysui.passThroughShellCommand") { + public void run() { + try { + if (mRegistry == null) { + return; + } + + // Registry blocks this thread until finished + mRegistry.onShellCommand(pw, args); + } finally { + pw.flush(); + try { + // Close the file descriptor so the TransferPipe finishes its thread + pfd.close(); + } catch (Exception e) { + } + } + } + }; + thr.start(); + } + private final class H extends Handler { private H(Looper l) { super(l); @@ -1154,6 +1201,10 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< mCallbacks.get(i).onCameraLaunchGestureDetected(msg.arg1); } break; + case MSG_EMERGENCY_ACTION_LAUNCH_GESTURE: + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).onEmergencyActionLaunchGestureDetected(); + } case MSG_SHOW_PICTURE_IN_PICTURE_MENU: for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).showPictureInPictureMenu(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java index ac3523b2fffd..1b1a51b8a57b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/MediaTransferManager.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar; import android.content.Context; -import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -36,10 +35,9 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.InfoMediaManager; import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; -import com.android.settingslib.media.MediaOutputSliceConstants; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.Dependency; -import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -51,7 +49,7 @@ import java.util.List; */ public class MediaTransferManager { private final Context mContext; - private final ActivityStarter mActivityStarter; + private final MediaOutputDialogFactory mMediaOutputDialogFactory; private MediaDevice mDevice; private List<View> mViews = new ArrayList<>(); private LocalMediaManager mLocalMediaManager; @@ -74,12 +72,7 @@ public class MediaTransferManager { ViewParent parent = view.getParent(); StatusBarNotification statusBarNotification = getRowForParent(parent).getEntry().getSbn(); - final Intent intent = new Intent() - .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) - .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME, - statusBarNotification.getPackageName()); - mActivityStarter.startActivity(intent, false, true /* dismissShade */, - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + mMediaOutputDialogFactory.create(statusBarNotification.getPackageName(), true); return true; } }; @@ -107,7 +100,7 @@ public class MediaTransferManager { public MediaTransferManager(Context context) { mContext = context; - mActivityStarter = Dependency.get(ActivityStarter.class); + mMediaOutputDialogFactory = Dependency.get(MediaOutputDialogFactory.class); LocalBluetoothManager lbm = Dependency.get(LocalBluetoothManager.class); InfoMediaManager imm = new InfoMediaManager(mContext, null, null, lbm); mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, null); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 8bf134d9c5b9..bb76ac0b26bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -40,6 +40,9 @@ import android.os.Trace; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; +import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationStats; +import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; import android.view.View; @@ -52,13 +55,18 @@ import com.android.systemui.Dumpable; import com.android.systemui.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.media.MediaData; import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; +import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.phone.BiometricUnlockController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.LockscreenWallpaper; @@ -77,6 +85,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import dagger.Lazy; @@ -106,6 +115,9 @@ public class NotificationMediaManager implements Dumpable { private final NotificationEntryManager mEntryManager; private final MediaDataManager mMediaDataManager; + private final NotifPipeline mNotifPipeline; + private final NotifCollection mNotifCollection; + private final boolean mUsingNotifPipeline; @Nullable private Lazy<NotificationShadeWindowController> mNotificationShadeWindowController; @@ -189,6 +201,9 @@ public class NotificationMediaManager implements Dumpable { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, + NotifPipeline notifPipeline, + NotifCollection notifCollection, + FeatureFlags featureFlags, @Main DelayableExecutor mainExecutor, DeviceConfigProxy deviceConfig, MediaDataManager mediaDataManager) { @@ -206,17 +221,87 @@ public class NotificationMediaManager implements Dumpable { mEntryManager = notificationEntryManager; mMainExecutor = mainExecutor; mMediaDataManager = mediaDataManager; + mNotifPipeline = notifPipeline; + mNotifCollection = notifCollection; - notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { + if (!featureFlags.isNewNotifPipelineRenderingEnabled()) { + setupNEM(); + mUsingNotifPipeline = false; + } else { + setupNotifPipeline(); + mUsingNotifPipeline = true; + } + + mShowCompactMediaSeekbar = "true".equals( + DeviceConfig.getProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.COMPACT_MEDIA_SEEKBAR_ENABLED)); + + deviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mContext.getMainExecutor(), + mPropertiesChangedListener); + } + + private void setupNotifPipeline() { + mNotifPipeline.addCollectionListener(new NotifCollectionListener() { + @Override + public void onEntryAdded(@NonNull NotificationEntry entry) { + mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + } + + @Override + public void onEntryUpdated(NotificationEntry entry) { + mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + } + + @Override + public void onEntryBind(NotificationEntry entry, StatusBarNotification sbn) { + findAndUpdateMediaNotifications(); + } + + @Override + public void onEntryRemoved(@NonNull NotificationEntry entry, int reason) { + removeEntry(entry); + } + + @Override + public void onEntryCleanUp(@NonNull NotificationEntry entry) { + removeEntry(entry); + } + }); + + mMediaDataManager.addListener(new MediaDataManager.Listener() { + @Override + public void onMediaDataLoaded(@NonNull String key, + @Nullable String oldKey, @NonNull MediaData data) { + } + + @Override + public void onMediaDataRemoved(@NonNull String key) { + mNotifPipeline.getAllNotifs() + .stream() + .filter(entry -> Objects.equals(entry.getKey(), key)) + .findAny() + .ifPresent(entry -> { + // TODO(b/160713608): "removing" this notification won't happen and + // won't send the 'deleteIntent' if the notification is ongoing. + mNotifCollection.dismissNotification(entry, + getDismissedByUserStats(entry)); + }); + } + }); + } + + private void setupNEM() { + mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { @Override public void onPendingEntryAdded(NotificationEntry entry) { - mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); } @Override public void onPreEntryUpdated(NotificationEntry entry) { - mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); + mMediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn()); } @Override @@ -231,8 +316,8 @@ public class NotificationMediaManager implements Dumpable { @Override public void onEntryRemoved( - NotificationEntry entry, - NotificationVisibility visibility, + @NonNull NotificationEntry entry, + @Nullable NotificationVisibility visibility, boolean removedByUser, int reason) { removeEntry(entry); @@ -242,20 +327,49 @@ public class NotificationMediaManager implements Dumpable { // Pending entries are never inflated, and will never generate a call to onEntryRemoved(). // This can happen when notifications are added and canceled before inflation. Add this // separate listener for cleanup, since media inflation occurs onPendingEntryAdded(). - notificationEntryManager.addCollectionListener(new NotifCollectionListener() { + mEntryManager.addCollectionListener(new NotifCollectionListener() { @Override public void onEntryCleanUp(@NonNull NotificationEntry entry) { removeEntry(entry); } }); - mShowCompactMediaSeekbar = "true".equals( - DeviceConfig.getProperty(DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.COMPACT_MEDIA_SEEKBAR_ENABLED)); + mMediaDataManager.addListener(new MediaDataManager.Listener() { + @Override + public void onMediaDataLoaded(@NonNull String key, + @Nullable String oldKey, @NonNull MediaData data) { + } - deviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, - mContext.getMainExecutor(), - mPropertiesChangedListener); + @Override + public void onMediaDataRemoved(@NonNull String key) { + NotificationEntry entry = mEntryManager.getPendingOrActiveNotif(key); + if (entry != null) { + // TODO(b/160713608): "removing" this notification won't happen and + // won't send the 'deleteIntent' if the notification is ongoing. + mEntryManager.performRemoveNotification(entry.getSbn(), + getDismissedByUserStats(entry), + NotificationListenerService.REASON_CANCEL); + } + } + }); + } + + private DismissedByUserStats getDismissedByUserStats(NotificationEntry entry) { + final int activeNotificationsCount; + if (mUsingNotifPipeline) { + activeNotificationsCount = mNotifPipeline.getShadeListCount(); + } else { + activeNotificationsCount = mEntryManager.getActiveNotificationsCount(); + } + return new DismissedByUserStats( + NotificationStats.DISMISSAL_SHADE, // Add DISMISSAL_MEDIA? + NotificationStats.DISMISS_SENTIMENT_NEUTRAL, + NotificationVisibility.obtain( + entry.getKey(), + entry.getRanking().getRank(), + activeNotificationsCount, + /* visible= */ true, + NotificationLogger.getNotificationLocation(entry))); } private void removeEntry(NotificationEntry entry) { @@ -299,14 +413,24 @@ public class NotificationMediaManager implements Dumpable { if (mMediaNotificationKey == null) { return null; } - synchronized (mEntryManager) { - NotificationEntry entry = mEntryManager + if (mUsingNotifPipeline) { + // TODO(b/169655596): Either add O(1) lookup, or cache this icon? + return mNotifPipeline.getAllNotifs().stream() + .filter(entry -> Objects.equals(entry.getKey(), mMediaNotificationKey)) + .findAny() + .map(entry -> entry.getIcons().getShelfIcon()) + .map(StatusBarIconView::getSourceIcon) + .orElse(null); + } else { + synchronized (mEntryManager) { + NotificationEntry entry = mEntryManager .getActiveNotificationUnfiltered(mMediaNotificationKey); - if (entry == null || entry.getIcons().getShelfIcon() == null) { - return null; - } + if (entry == null || entry.getIcons().getShelfIcon() == null) { + return null; + } - return entry.getIcons().getShelfIcon().getSourceIcon(); + return entry.getIcons().getShelfIcon().getSourceIcon(); + } } } @@ -321,94 +445,110 @@ public class NotificationMediaManager implements Dumpable { } public void findAndUpdateMediaNotifications() { + boolean metaDataChanged; + if (mUsingNotifPipeline) { + // TODO(b/169655907): get the semi-filtered notifications for current user + Collection<NotificationEntry> allNotifications = mNotifPipeline.getAllNotifs(); + metaDataChanged = findPlayingMediaNotification(allNotifications); + } else { + synchronized (mEntryManager) { + Collection<NotificationEntry> allNotifications = mEntryManager.getAllNotifs(); + metaDataChanged = findPlayingMediaNotification(allNotifications); + } + + if (metaDataChanged) { + mEntryManager.updateNotifications("NotificationMediaManager - metaDataChanged"); + } + + } + dispatchUpdateMediaMetaData(metaDataChanged, true /* allowEnterAnimation */); + } + + /** + * Find a notification and media controller associated with the playing media session, and + * update this manager's internal state. + * @return whether the current MediaMetadata changed (and needs to be announced to listeners). + */ + private boolean findPlayingMediaNotification( + @NonNull Collection<NotificationEntry> allNotifications) { boolean metaDataChanged = false; + // Promote the media notification with a controller in 'playing' state, if any. + NotificationEntry mediaNotification = null; + MediaController controller = null; + for (NotificationEntry entry : allNotifications) { + if (entry.isMediaNotification()) { + final MediaSession.Token token = + entry.getSbn().getNotification().extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION); + if (token != null) { + MediaController aController = new MediaController(mContext, token); + if (PlaybackState.STATE_PLAYING + == getMediaControllerPlaybackState(aController)) { + if (DEBUG_MEDIA) { + Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching " + + entry.getSbn().getKey()); + } + mediaNotification = entry; + controller = aController; + break; + } + } + } + } + if (mediaNotification == null) { + // Still nothing? OK, let's just look for live media sessions and see if they match + // one of our notifications. This will catch apps that aren't (yet!) using media + // notifications. + + if (mMediaSessionManager != null) { + // TODO: Should this really be for all users? It appears that inactive users + // can't have active sessions, which would mean it is fine. + final List<MediaController> sessions = + mMediaSessionManager.getActiveSessionsForUser(null, UserHandle.USER_ALL); - synchronized (mEntryManager) { - Collection<NotificationEntry> allNotifications = mEntryManager.getAllNotifs(); - - // Promote the media notification with a controller in 'playing' state, if any. - NotificationEntry mediaNotification = null; - MediaController controller = null; - for (NotificationEntry entry : allNotifications) { - if (entry.isMediaNotification()) { - final MediaSession.Token token = - entry.getSbn().getNotification().extras.getParcelable( - Notification.EXTRA_MEDIA_SESSION); - if (token != null) { - MediaController aController = new MediaController(mContext, token); - if (PlaybackState.STATE_PLAYING == - getMediaControllerPlaybackState(aController)) { + for (MediaController aController : sessions) { + // now to see if we have one like this + final String pkg = aController.getPackageName(); + + for (NotificationEntry entry : allNotifications) { + if (entry.getSbn().getPackageName().equals(pkg)) { if (DEBUG_MEDIA) { - Log.v(TAG, "DEBUG_MEDIA: found mediastyle controller matching " + Log.v(TAG, "DEBUG_MEDIA: found controller matching " + entry.getSbn().getKey()); } - mediaNotification = entry; controller = aController; + mediaNotification = entry; break; } } } } - if (mediaNotification == null) { - // Still nothing? OK, let's just look for live media sessions and see if they match - // one of our notifications. This will catch apps that aren't (yet!) using media - // notifications. - - if (mMediaSessionManager != null) { - // TODO: Should this really be for all users? - final List<MediaController> sessions - = mMediaSessionManager.getActiveSessionsForUser( - null, - UserHandle.USER_ALL); - - for (MediaController aController : sessions) { - // now to see if we have one like this - final String pkg = aController.getPackageName(); - - for (NotificationEntry entry : allNotifications) { - if (entry.getSbn().getPackageName().equals(pkg)) { - if (DEBUG_MEDIA) { - Log.v(TAG, "DEBUG_MEDIA: found controller matching " - + entry.getSbn().getKey()); - } - controller = aController; - mediaNotification = entry; - break; - } - } - } - } - } - - if (controller != null && !sameSessions(mMediaController, controller)) { - // We have a new media session - clearCurrentMediaNotificationSession(); - mMediaController = controller; - mMediaController.registerCallback(mMediaListener); - mMediaMetadata = mMediaController.getMetadata(); - if (DEBUG_MEDIA) { - Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: " - + mMediaController + ", receive metadata: " + mMediaMetadata); - } + } - metaDataChanged = true; + if (controller != null && !sameSessions(mMediaController, controller)) { + // We have a new media session + clearCurrentMediaNotificationSession(); + mMediaController = controller; + mMediaController.registerCallback(mMediaListener); + mMediaMetadata = mMediaController.getMetadata(); + if (DEBUG_MEDIA) { + Log.v(TAG, "DEBUG_MEDIA: insert listener, found new controller: " + + mMediaController + ", receive metadata: " + mMediaMetadata); } - if (mediaNotification != null - && !mediaNotification.getSbn().getKey().equals(mMediaNotificationKey)) { - mMediaNotificationKey = mediaNotification.getSbn().getKey(); - if (DEBUG_MEDIA) { - Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key=" - + mMediaNotificationKey); - } - } + metaDataChanged = true; } - if (metaDataChanged) { - mEntryManager.updateNotifications("NotificationMediaManager - metaDataChanged"); + if (mediaNotification != null + && !mediaNotification.getSbn().getKey().equals(mMediaNotificationKey)) { + mMediaNotificationKey = mediaNotification.getSbn().getKey(); + if (DEBUG_MEDIA) { + Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key=" + + mMediaNotificationKey); + } } - dispatchUpdateMediaMetaData(metaDataChanged, true /* allowEnterAnimation */); + return metaDataChanged; } public void clearCurrentMediaNotification() { @@ -428,7 +568,7 @@ public class NotificationMediaManager implements Dumpable { } @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { pw.print(" mMediaSessionManager="); pw.println(mMediaSessionManager); pw.print(" mMediaNotificationKey="); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index c1196d65b702..01aa53f14550 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -189,7 +189,11 @@ class NotificationShadeDepthController @Inject constructor( blurUtils.applyBlur(blurRoot?.viewRootImpl ?: root.viewRootImpl, blur) val zoomOut = blurUtils.ratioOfBlurRadius(blur) try { - wallpaperManager.setWallpaperZoomOut(root.windowToken, zoomOut) + if (root.isAttachedToWindow) { + wallpaperManager.setWallpaperZoomOut(root.windowToken, zoomOut) + } else { + Log.i(TAG, "Won't set zoom. Window not attached $root") + } } catch (e: IllegalArgumentException) { Log.w(TAG, "Can't set zoom. Window is gone: ${root.windowToken}", e) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 38c7e5c50f63..53179ba4be90 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -26,7 +26,7 @@ import android.view.View; import android.view.ViewGroup; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; @@ -47,6 +47,7 @@ import com.android.systemui.util.Assert; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Optional; import java.util.Stack; /** @@ -84,7 +85,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle * possible. */ private final boolean mAlwaysExpandNonGroupedNotification; - private final BubbleController mBubbleController; + private final Optional<Bubbles> mBubblesOptional; private final DynamicPrivacyController mDynamicPrivacyController; private final KeyguardBypassController mBypassController; private final ForegroundServiceSectionController mFgsSectionController; @@ -112,7 +113,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle StatusBarStateController statusBarStateController, NotificationEntryManager notificationEntryManager, KeyguardBypassController bypassController, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, DynamicPrivacyController privacyController, ForegroundServiceSectionController fgsSectionController, DynamicChildBindController dynamicChildBindController, @@ -130,7 +131,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle Resources res = context.getResources(); mAlwaysExpandNonGroupedNotification = res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications); - mBubbleController = bubbleController; + mBubblesOptional = bubblesOptional; mDynamicPrivacyController = privacyController; mDynamicChildBindController = dynamicChildBindController; mLowPriorityInflationHelper = lowPriorityInflationHelper; @@ -157,8 +158,10 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle final int N = activeNotifications.size(); for (int i = 0; i < N; i++) { NotificationEntry ent = activeNotifications.get(i); + final boolean isBubbleNotificationSuppressedFromShade = mBubblesOptional.isPresent() + && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade(ent); if (ent.isRowDismissed() || ent.isRowRemoved() - || mBubbleController.isBubbleNotificationSuppressedFromShade(ent) + || isBubbleNotificationSuppressedFromShade || mFgsSectionController.hasEntry(ent)) { // we don't want to update removed notifications because they could // temporarily become children if they were isolated before. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/commandline/CommandRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/CommandRegistry.kt new file mode 100644 index 000000000000..ce0a08cd4ccf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/CommandRegistry.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2020 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.commandline + +import android.content.Context + +import com.android.systemui.Prefs +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main + +import java.io.PrintWriter +import java.lang.IllegalStateException +import java.util.concurrent.Executor +import java.util.concurrent.FutureTask + +import javax.inject.Inject + +/** + * Registry / dispatcher for incoming shell commands. See [StatusBarManagerService] and + * [StatusBarShellCommand] for how things are set up. Commands come in here by way of the service + * like so: + * + * `adb shell cmd statusbar <command>` + * + * Where `cmd statusbar` send the shell command through to StatusBarManagerService, and + * <command> is either processed in system server, or sent through to IStatusBar (CommandQueue) + */ +@SysUISingleton +class CommandRegistry @Inject constructor( + val context: Context, + @Main val mainExecutor: Executor +) { + // To keep the command line parser hermetic, create a new one for every shell command + private val commandMap = mutableMapOf<String, CommandWrapper>() + private var initialized = false + + /** + * Register a [Command] for a given name. The name here is the top-level namespace for + * the registered command. A command could look like this for instance: + * + * `adb shell cmd statusbar notifications list` + * + * Where `notifications` is the command that signifies which receiver to send the remaining args + * to. + * + * @param command String name of the command to register. Currently does not support aliases + * @param receiverFactory Creates an instance of the receiver on every command + * @param executor Pass an executor to offload your `receive` to another thread + */ + @Synchronized + fun registerCommand( + name: String, + commandFactory: () -> Command, + executor: Executor + ) { + if (commandMap[name] != null) { + throw IllegalStateException("A command is already registered for ($name)") + } + commandMap[name] = CommandWrapper(commandFactory, executor) + } + + /** + * Register a [Command] for a given name, to be executed on the main thread. + */ + @Synchronized + fun registerCommand(name: String, commandFactory: () -> Command) { + registerCommand(name, commandFactory, mainExecutor) + } + + /** Unregister a receiver */ + @Synchronized + fun unregisterCommand(command: String) { + commandMap.remove(command) + } + + private fun initializeCommands() { + initialized = true + // TODO: Might want a dedicated place for commands without a home. Currently + // this is here because Prefs.java is just an interface + registerCommand("prefs") { PrefsCommand(context) } + } + + /** + * Receive a shell command and dispatch to the appropriate [Command]. Blocks until finished. + */ + fun onShellCommand(pw: PrintWriter, args: Array<String>) { + if (!initialized) initializeCommands() + + if (args.isEmpty()) { + help(pw) + return + } + + val commandName = args[0] + val wrapper = commandMap[commandName] + + if (wrapper == null) { + help(pw) + return + } + + // Create a new instance of the command + val command = wrapper.commandFactory() + + // Wrap the receive command in a task so that we can wait for its completion + val task = FutureTask<Unit> { + command.execute(pw, args.drop(1)) + } + + wrapper.executor.execute { + task.run() + } + + // Wait for the future to complete + task.get() + } + + private fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar <command>") + pw.println(" known commands:") + for (k in commandMap.keys) { + pw.println(" $k") + } + } +} + +private const val TAG = "CommandRegistry" + +interface Command { + fun execute(pw: PrintWriter, args: List<String>) + fun help(pw: PrintWriter) +} + +// Wrap commands in an executor package +private data class CommandWrapper(val commandFactory: () -> Command, val executor: Executor) + +// Commands can go here for now, but they should move outside + +private class PrefsCommand(val context: Context) : Command { + override fun help(pw: PrintWriter) { + pw.println("usage: prefs <command> [args]") + pw.println("Available commands:") + pw.println(" list-prefs") + pw.println(" set-pref <pref name> <value>") + } + + override fun execute(pw: PrintWriter, args: List<String>) { + if (args.isEmpty()) { + help(pw) + return + } + + val topLevel = args[0] + + when (topLevel) { + "list-prefs" -> listPrefs(pw) + "set-pref" -> setPref(pw, args.drop(1)) + else -> help(pw) + } + } + + private fun listPrefs(pw: PrintWriter) { + pw.println("Available keys:") + for (field in Prefs.Key::class.java.declaredFields) { + pw.print(" ") + pw.println(field.get(Prefs.Key::class.java)) + } + } + + /** + * Sets a preference from [Prefs] + */ + private fun setPref(pw: PrintWriter, args: List<String>) { + if (args.isEmpty()) { + pw.println("invalid arguments: $args") + return + } + val pref = args[0] + + when (pref) { + Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING -> { + val value = Integer.parseInt(args[1]) + Prefs.putBoolean(context, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, value != 0) + } + else -> { + pw.println("Cannot set pref ($pref)") + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index d15b8476b3c5..cee9c70f53eb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -21,13 +21,14 @@ import android.content.Context; import android.os.Handler; import com.android.internal.statusbar.IStatusBarService; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.media.MediaDataManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.ActionClickLogger; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.MediaArtworkProcessor; import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationListener; @@ -39,10 +40,13 @@ import com.android.systemui.statusbar.NotificationViewHierarchyManager; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.notification.AssistantFeedbackController; import com.android.systemui.statusbar.notification.DynamicChildBindController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.inflation.LowPriorityInflationHelper; import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; @@ -59,6 +63,8 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.concurrency.DelayableExecutor; +import java.util.Optional; + import dagger.Binds; import dagger.Lazy; import dagger.Module; @@ -108,6 +114,9 @@ public interface StatusBarDependenciesModule { NotificationEntryManager notificationEntryManager, MediaArtworkProcessor mediaArtworkProcessor, KeyguardBypassController keyguardBypassController, + NotifPipeline notifPipeline, + NotifCollection notifCollection, + FeatureFlags featureFlags, @Main DelayableExecutor mainExecutor, DeviceConfigProxy deviceConfigProxy, MediaDataManager mediaDataManager) { @@ -118,6 +127,9 @@ public interface StatusBarDependenciesModule { notificationEntryManager, mediaArtworkProcessor, keyguardBypassController, + notifPipeline, + notifCollection, + featureFlags, mainExecutor, deviceConfigProxy, mediaDataManager); @@ -162,7 +174,7 @@ public interface StatusBarDependenciesModule { StatusBarStateController statusBarStateController, NotificationEntryManager notificationEntryManager, KeyguardBypassController bypassController, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, DynamicPrivacyController privacyController, ForegroundServiceSectionController fgsSectionController, DynamicChildBindController dynamicChildBindController, @@ -177,7 +189,7 @@ public interface StatusBarDependenciesModule { statusBarStateController, notificationEntryManager, bypassController, - bubbleController, + bubblesOptional, privacyController, fgsSectionController, dynamicChildBindController, @@ -190,8 +202,11 @@ public interface StatusBarDependenciesModule { */ @Provides @SysUISingleton - static CommandQueue provideCommandQueue(Context context, ProtoTracer protoTracer) { - return new CommandQueue(context, protoTracer); + static CommandQueue provideCommandQueue( + Context context, + ProtoTracer protoTracer, + CommandRegistry registry) { + return new CommandQueue(context, protoTracer, registry); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index 433c8b0d361d..ddfa18e65ee0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -27,10 +27,10 @@ import com.android.internal.widget.ConversationLayout import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationContentView import com.android.systemui.statusbar.notification.stack.StackStateAnimator -import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -85,38 +85,43 @@ class ConversationNotificationManager @Inject constructor( for (entry in activeConversationEntries) { if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) { val important = ranking.channel.isImportantConversation - val layouts = entry.row?.layouts?.asSequence() + var changed = false + entry.row?.layouts?.asSequence() ?.flatMap(::getLayouts) ?.mapNotNull { it as? ConversationLayout } - ?: emptySequence() - var changed = false - for (layout in layouts) { - if (important == layout.isImportantConversation) { - continue - } - changed = true - if (important && entry.isMarkedForUserTriggeredMovement) { - // delay this so that it doesn't animate in until after - // the notif has been moved in the shade - mainHandler.postDelayed({ - layout.setIsImportantConversation( - important, true /* animate */) - }, IMPORTANCE_ANIMATION_DELAY.toLong()) - } else { - layout.setIsImportantConversation(important) - } - } + ?.filterNot { it.isImportantConversation == important } + ?.forEach { layout -> + changed = true + if (important && entry.isMarkedForUserTriggeredMovement) { + // delay this so that it doesn't animate in until after + // the notif has been moved in the shade + mainHandler.postDelayed( + { + layout.setIsImportantConversation( + important, + true) + }, + IMPORTANCE_ANIMATION_DELAY.toLong()) + } else { + layout.setIsImportantConversation(important, false) + } + } if (changed) { notificationGroupManager.updateIsolation(entry) + // ensure that the conversation icon isn't hidden + // (ex: if it was showing in the shelf) + entry.row?.updateIconVisibilities() } } } } override fun onEntryInflated(entry: NotificationEntry) { - if (!entry.ranking.isConversation) return + if (!entry.ranking.isConversation) { + return + } fun updateCount(isExpanded: Boolean) { - if (isExpanded && (!notifPanelCollapsed || entry.isPinnedAndExpanded())) { + if (isExpanded && (!notifPanelCollapsed || entry.isPinnedAndExpanded)) { resetCount(entry.key) entry.row?.let(::resetBadgeUi) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index d364689a65d4..7d8979ca1129 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -22,7 +22,7 @@ import android.util.Log; import android.view.View; import com.android.systemui.DejankUtils; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.phone.StatusBar; @@ -38,19 +38,19 @@ import javax.inject.Inject; public final class NotificationClicker implements View.OnClickListener { private static final String TAG = "NotificationClicker"; - private final BubbleController mBubbleController; private final NotificationClickerLogger mLogger; - private final Optional<StatusBar> mStatusBar; + private final Optional<StatusBar> mStatusBarOptional; + private final Optional<Bubbles> mBubblesOptional; private final NotificationActivityStarter mNotificationActivityStarter; private NotificationClicker( - BubbleController bubbleController, NotificationClickerLogger logger, - Optional<StatusBar> statusBar, + Optional<StatusBar> statusBarOptional, + Optional<Bubbles> bubblesOptional, NotificationActivityStarter notificationActivityStarter) { - mBubbleController = bubbleController; mLogger = logger; - mStatusBar = statusBar; + mStatusBarOptional = statusBarOptional; + mBubblesOptional = bubblesOptional; mNotificationActivityStarter = notificationActivityStarter; } @@ -61,7 +61,7 @@ public final class NotificationClicker implements View.OnClickListener { return; } - mStatusBar.ifPresent(statusBar -> statusBar.wakeUpIfDozing( + mStatusBarOptional.ifPresent(statusBar -> statusBar.wakeUpIfDozing( SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK")); final ExpandableNotificationRow row = (ExpandableNotificationRow) v; @@ -92,8 +92,8 @@ public final class NotificationClicker implements View.OnClickListener { row.setJustClicked(true); DejankUtils.postAfterTraversal(() -> row.setJustClicked(false)); - if (!row.getEntry().isBubble()) { - mBubbleController.collapseStack(); + if (!row.getEntry().isBubble() && mBubblesOptional.isPresent()) { + mBubblesOptional.get().collapseStack(); } mNotificationActivityStarter.onNotificationClicked(entry.getSbn(), row); @@ -118,26 +118,23 @@ public final class NotificationClicker implements View.OnClickListener { /** Daggerized builder for NotificationClicker. */ public static class Builder { - private final BubbleController mBubbleController; private final NotificationClickerLogger mLogger; @Inject - public Builder( - BubbleController bubbleController, - NotificationClickerLogger logger) { - mBubbleController = bubbleController; + public Builder(NotificationClickerLogger logger) { mLogger = logger; } /** Builds an instance. */ public NotificationClicker build( - Optional<StatusBar> statusBar, + Optional<StatusBar> statusBarOptional, + Optional<Bubbles> bubblesOptional, NotificationActivityStarter notificationActivityStarter ) { return new NotificationClicker( - mBubbleController, mLogger, - statusBar, + statusBarOptional, + bubblesOptional, notificationActivityStarter); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index fdfd72489e93..d617dff372da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -291,6 +291,7 @@ public class NotifCollection implements Dumpable { mLogger.logDismissAll(userId); try { + // TODO(b/169585328): Do not clear media player notifications mStatusBarService.onClearAllNotifications(userId); } catch (RemoteException e) { // system process is dead if we're here. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java index 4ddc1dc8498d..0455b0f18afc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BubbleCoordinator.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator; import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -26,6 +27,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Di import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import javax.inject.Inject; @@ -54,7 +56,7 @@ import javax.inject.Inject; public class BubbleCoordinator implements Coordinator { private static final String TAG = "BubbleCoordinator"; - private final BubbleController mBubbleController; + private final Optional<Bubbles> mBubblesOptional; private final NotifCollection mNotifCollection; private final Set<String> mInterceptedDismissalEntries = new HashSet<>(); private NotifPipeline mNotifPipeline; @@ -62,9 +64,9 @@ public class BubbleCoordinator implements Coordinator { @Inject public BubbleCoordinator( - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, NotifCollection notifCollection) { - mBubbleController = bubbleController; + mBubblesOptional = bubblesOptional; mNotifCollection = notifCollection; } @@ -73,13 +75,17 @@ public class BubbleCoordinator implements Coordinator { mNotifPipeline = pipeline; mNotifPipeline.addNotificationDismissInterceptor(mDismissInterceptor); mNotifPipeline.addFinalizeFilter(mNotifFilter); - mBubbleController.addNotifCallback(mNotifCallback); + if (mBubblesOptional.isPresent()) { + mBubblesOptional.get().addNotifCallback(mNotifCallback); + } + } private final NotifFilter mNotifFilter = new NotifFilter(TAG) { @Override public boolean shouldFilterOut(NotificationEntry entry, long now) { - return mBubbleController.isBubbleNotificationSuppressedFromShade(entry); + return mBubblesOptional.isPresent() + && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade(entry); } }; @@ -97,7 +103,8 @@ public class BubbleCoordinator implements Coordinator { @Override public boolean shouldInterceptDismissal(NotificationEntry entry) { // for experimental bubbles - if (mBubbleController.handleDismissalInterception(entry)) { + if (mBubblesOptional.isPresent() + && mBubblesOptional.get().handleDismissalInterception(entry)) { mInterceptedDismissalEntries.add(entry.getKey()); return true; } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java index 21d54c85160b..490989dbb39e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java @@ -21,9 +21,8 @@ import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; -import com.android.systemui.Dependency; import com.android.systemui.Dumpable; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; @@ -43,6 +42,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import javax.inject.Inject; @@ -64,25 +64,20 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, new ArraySet<>(); private final ArraySet<OnGroupChangeListener> mGroupChangeListeners = new ArraySet<>(); private final Lazy<PeopleNotificationIdentifier> mPeopleNotificationIdentifier; + private final Optional<Lazy<Bubbles>> mBubblesOptional; private int mBarState = -1; private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>(); private HeadsUpManager mHeadsUpManager; private boolean mIsUpdatingUnchangedGroup; - @Nullable private BubbleController mBubbleController = null; @Inject public NotificationGroupManagerLegacy( StatusBarStateController statusBarStateController, - Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier) { + Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier, + Optional<Lazy<Bubbles>> bubblesOptional) { statusBarStateController.addCallback(this); mPeopleNotificationIdentifier = peopleNotificationIdentifier; - } - - private BubbleController getBubbleController() { - if (mBubbleController == null) { - mBubbleController = Dependency.get(BubbleController.class); - } - return mBubbleController; + mBubblesOptional = bubblesOptional; } /** @@ -247,7 +242,8 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, int childCount = 0; boolean hasBubbles = false; for (NotificationEntry entry : group.children.values()) { - if (!getBubbleController().isBubbleNotificationSuppressedFromShade(entry)) { + if (mBubblesOptional.isPresent() && !mBubblesOptional.get().get() + .isBubbleNotificationSuppressedFromShade(entry)) { childCount++; } else { hasBubbles = true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt index 498b8e884b17..1311e3e756dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt @@ -17,11 +17,13 @@ package com.android.systemui.statusbar.notification.collection.render import android.annotation.StringRes +import android.content.Intent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.android.systemui.R -import com.android.systemui.statusbar.notification.dagger.HeaderClick +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.notification.dagger.HeaderClickAction import com.android.systemui.statusbar.notification.dagger.HeaderText import com.android.systemui.statusbar.notification.dagger.NodeLabel import com.android.systemui.statusbar.notification.dagger.SectionHeaderScope @@ -39,11 +41,19 @@ internal class SectionHeaderNodeControllerImpl @Inject constructor( @NodeLabel override val nodeLabel: String, private val layoutInflater: LayoutInflater, @HeaderText @StringRes private val headerTextResId: Int, - @HeaderClick private val onHeaderClickListener: View.OnClickListener + private val activityStarter: ActivityStarter, + @HeaderClickAction private val clickIntentAction: String ) : NodeController, SectionHeaderController { private var _view: SectionHeaderView? = null private var clearAllClickListener: View.OnClickListener? = null + private val onHeaderClickListener = View.OnClickListener { + activityStarter.startActivity( + Intent(clickIntentAction), + true /* onlyProvisioned */, + true /* dismissShade */, + Intent.FLAG_ACTIVITY_SINGLE_TOP) + } override fun reinflateView(parent: ViewGroup) { var oldPos = -1 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt index 22ca4961320c..7babbb40b6c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt @@ -172,14 +172,19 @@ class ShadeViewDiffer( private fun treeToMap(tree: NodeSpec): Map<NodeController, NodeSpec> { val map = mutableMapOf<NodeController, NodeSpec>() - registerNodes(tree, map) + try { + registerNodes(tree, map) + } catch (ex: DuplicateNodeException) { + logger.logDuplicateNodeInTree(tree, ex) + throw ex + } return map } private fun registerNodes(node: NodeSpec, map: MutableMap<NodeController, NodeSpec>) { if (map.containsKey(node.controller)) { - throw RuntimeException("Node ${node.controller.nodeLabel} appears more than once") + throw DuplicateNodeException("Node ${node.controller.nodeLabel} appears more than once") } map[node.controller] = node @@ -191,6 +196,8 @@ class ShadeViewDiffer( } } +private class DuplicateNodeException(message: String) : RuntimeException(message) + private class ShadeNode( val controller: NodeController ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt index 19e156f572d4..d27455004c01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.render import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogLevel import com.android.systemui.log.dagger.NotificationLog +import java.lang.RuntimeException import javax.inject.Inject class ShadeViewDifferLogger @Inject constructor( @@ -67,6 +68,15 @@ class ShadeViewDifferLogger @Inject constructor( "Moving child view $str1 in $str2 to index $int1" }) } + + fun logDuplicateNodeInTree(node: NodeSpec, ex: RuntimeException) { + buffer.log(TAG, LogLevel.ERROR, { + str1 = ex.toString() + str2 = treeSpecToStr(node) + }, { + "$str1 when mapping tree: $str2" + }) + } } private const val TAG = "NotifViewManager"
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt index 179d49cb55a1..2a9cfd034dce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt @@ -17,12 +17,9 @@ package com.android.systemui.statusbar.notification.dagger import android.annotation.StringRes -import android.content.Intent import android.provider.Settings -import android.view.View import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController import com.android.systemui.statusbar.notification.collection.render.SectionHeaderNodeControllerImpl @@ -39,18 +36,6 @@ import javax.inject.Scope object NotificationSectionHeadersModule { @Provides - @HeaderClick - @JvmStatic fun providesOnHeaderClickListener( - activityStarter: ActivityStarter - ) = View.OnClickListener { - activityStarter.startActivity( - Intent(Settings.ACTION_NOTIFICATION_SETTINGS), - true /* onlyProvisioned */, - true /* dismissShade */, - Intent.FLAG_ACTIVITY_SINGLE_TOP) - } - - @Provides @IncomingHeader @SysUISingleton @JvmStatic fun providesIncomingHeaderSubcomponent( @@ -58,6 +43,7 @@ object NotificationSectionHeadersModule { ) = builder.get() .nodeLabel("incoming header") .headerText(R.string.notification_section_header_incoming) + .clickIntentAction(Settings.ACTION_NOTIFICATION_SETTINGS) .build() @Provides @@ -68,6 +54,7 @@ object NotificationSectionHeadersModule { ) = builder.get() .nodeLabel("alerting header") .headerText(R.string.notification_section_header_alerting) + .clickIntentAction(Settings.ACTION_NOTIFICATION_SETTINGS) .build() @Provides @@ -78,6 +65,7 @@ object NotificationSectionHeadersModule { ) = builder.get() .nodeLabel("people header") .headerText(R.string.notification_section_header_conversations) + .clickIntentAction(Settings.ACTION_CONVERSATION_SETTINGS) .build() @Provides @@ -88,6 +76,7 @@ object NotificationSectionHeadersModule { ) = builder.get() .nodeLabel("silent header") .headerText(R.string.notification_section_header_gentle) + .clickIntentAction(Settings.ACTION_NOTIFICATION_SETTINGS) .build() @Provides @@ -151,6 +140,7 @@ interface SectionHeaderControllerSubcomponent { fun build(): SectionHeaderControllerSubcomponent @BindsInstance fun nodeLabel(@NodeLabel nodeLabel: String): Builder @BindsInstance fun headerText(@HeaderText @StringRes headerText: Int): Builder + @BindsInstance fun clickIntentAction(@HeaderClickAction clickIntentAction: String): Builder } } @@ -188,7 +178,7 @@ annotation class NodeLabel @Qualifier @Retention(AnnotationRetention.BINARY) -annotation class HeaderClick +annotation class HeaderClickAction @Scope @Retention(AnnotationRetention.BINARY) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 01333f0a47d5..4fff99b482d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -26,7 +26,7 @@ import android.view.accessibility.AccessibilityManager; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -74,6 +74,7 @@ import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.util.leak.LeakDetector; +import java.util.Optional; import java.util.concurrent.Executor; import javax.inject.Provider; @@ -132,7 +133,7 @@ public interface NotificationsModule { UserContextProvider contextTracker, Provider<PriorityOnboardingDialogController.Builder> builderProvider, AssistantFeedbackController assistantFeedbackController, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, UiEventLogger uiEventLogger, OnUserInteractionCallback onUserInteractionCallback) { return new NotificationGutsManager( @@ -149,7 +150,7 @@ public interface NotificationsModule { contextTracker, builderProvider, assistantFeedbackController, - bubbleController, + bubblesOptional, uiEventLogger, onUserInteractionCallback); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt index 9da8b8a3fd92..049b471aa7cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification +import com.android.systemui.bubbles.Bubbles import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.NotificationActivityStarter @@ -25,6 +26,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationListContain import com.android.systemui.statusbar.phone.StatusBar import java.io.FileDescriptor import java.io.PrintWriter +import java.util.Optional /** * The master controller for all notifications-related work @@ -35,6 +37,7 @@ import java.io.PrintWriter interface NotificationsController { fun initialize( statusBar: StatusBar, + bubblesOptional: Optional<Bubbles>, presenter: NotificationPresenter, listContainer: NotificationListContainer, notificationActivityStarter: NotificationActivityStarter, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 9fb292878553..45a5d1044b9a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification +import com.android.systemui.bubbles.Bubbles import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.FeatureFlags @@ -75,6 +76,7 @@ class NotificationsControllerImpl @Inject constructor( override fun initialize( statusBar: StatusBar, + bubblesOptional: Optional<Bubbles>, presenter: NotificationPresenter, listContainer: NotificationListContainer, notificationActivityStarter: NotificationActivityStarter, @@ -90,7 +92,8 @@ class NotificationsControllerImpl @Inject constructor( listController.bind() notificationRowBinder.setNotificationClicker( - clickerBuilder.build(Optional.of(statusBar), notificationActivityStarter)) + clickerBuilder.build( + Optional.of(statusBar), bubblesOptional, notificationActivityStarter)) notificationRowBinder.setUpWithPresenter( presenter, listContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt index ded855dd84f9..7569c1bdbb73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification +import com.android.systemui.bubbles.Bubbles import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener import com.android.systemui.statusbar.NotificationPresenter @@ -26,6 +27,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationListContain import com.android.systemui.statusbar.phone.StatusBar import java.io.FileDescriptor import java.io.PrintWriter +import java.util.Optional import javax.inject.Inject /** @@ -37,6 +39,7 @@ class NotificationsControllerStub @Inject constructor( override fun initialize( statusBar: StatusBar, + bubblesOptional: Optional<Bubbles>, presenter: NotificationPresenter, listContainer: NotificationListContainer, notificationActivityStarter: NotificationActivityStarter, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index f1727ec91c72..094e8661d262 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -124,6 +124,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView private float mAppearAnimationTranslation; private int mNormalColor; private boolean mIsBelowSpeedBump; + private long mLastActionUpTime; private float mNormalBackgroundVisibilityAmount; private float mDimmedBackgroundFadeInAmount = -1; @@ -225,6 +226,22 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView return super.onInterceptTouchEvent(ev); } + /** Sets the last action up time this view was touched. */ + void setLastActionUpTime(long eventTime) { + mLastActionUpTime = eventTime; + } + + /** + * Returns the last action up time. The last time will also be cleared because the source of + * action is not only from touch event. That prevents the caller from utilizing the time with + * unrelated event. The time can be 0 if the event is unavailable. + */ + public long getAndResetLastActionUpTime() { + long lastActionUpTime = mLastActionUpTime; + mLastActionUpTime = 0; + return lastActionUpTime; + } + protected boolean disallowSingleClick(MotionEvent ev) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java index dd30c890e75b..41ce51c2762f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row; +import android.os.SystemClock; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityManager; @@ -92,6 +93,9 @@ public class ActivatableNotificationViewController { mBlockNextTouch = false; return true; } + if (ev.getAction() == MotionEvent.ACTION_UP) { + mView.setLastActionUpTime(SystemClock.uptimeMillis()); + } if (mNeedsDimming && !mAccessibilityManager.isTouchExplorationEnabled() && mView.isInteractive()) { if (mNeedsDimming && !mView.isDimmed()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 811a72de093c..d8d412bf2d41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -72,7 +72,7 @@ import com.android.internal.widget.CachingIconView; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -1074,7 +1074,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return new View.OnClickListener() { @Override public void onClick(View v) { - Dependency.get(BubbleController.class) + Dependency.get(Bubbles.class) .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); } @@ -1478,8 +1478,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - private void updateIconVisibilities() { - // The shelficon is never hidden for children in groups + /** Refreshes the visibility of notification icons */ + public void updateIconVisibilities() { + // The shelf icon is never hidden for children in groups boolean visible = !isChildInGroup() && mShelfIconVisible; for (NotificationContentView l : mLayouts) { l.setShelfIconVisible(visible); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 1d72557c6a89..c995e324ecfe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -81,21 +81,27 @@ public class ExpandableNotificationRowController implements NodeController { private final PeopleNotificationIdentifier mPeopleNotificationIdentifier; @Inject - public ExpandableNotificationRowController(ExpandableNotificationRow view, + public ExpandableNotificationRowController( + ExpandableNotificationRow view, NotificationListContainer listContainer, ActivatableNotificationViewController activatableNotificationViewController, - NotificationMediaManager mediaManager, PluginManager pluginManager, - SystemClock clock, @AppName String appName, @NotificationKey String notificationKey, + NotificationMediaManager mediaManager, + PluginManager pluginManager, + SystemClock clock, + @AppName String appName, + @NotificationKey String notificationKey, KeyguardBypassController keyguardBypassController, GroupMembershipManager groupMembershipManager, GroupExpansionManager groupExpansionManager, RowContentBindStage rowContentBindStage, - NotificationLogger notificationLogger, HeadsUpManager headsUpManager, + NotificationLogger notificationLogger, + HeadsUpManager headsUpManager, ExpandableNotificationRow.OnExpandClickListener onExpandClickListener, StatusBarStateController statusBarStateController, NotificationGutsManager notificationGutsManager, @Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME) boolean allowLongPress, - OnUserInteractionCallback onUserInteractionCallback, FalsingManager falsingManager, + OnUserInteractionCallback onUserInteractionCallback, + FalsingManager falsingManager, PeopleNotificationIdentifier peopleNotificationIdentifier) { mView = view; mListContainer = listContainer; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 9bcac1163acc..c2c4590fa6cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -42,17 +42,15 @@ import com.android.systemui.media.MediaDataManagerKt; import com.android.systemui.media.MediaFeatureFlag; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.NotificationRemoteInputManager; -import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.MediaNotificationProcessor; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.phone.StatusBar; -import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.InflatedSmartReplies; import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions; -import com.android.systemui.statusbar.policy.SmartReplyConstants; +import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflater; import com.android.systemui.util.Assert; import java.util.HashMap; @@ -60,8 +58,6 @@ import java.util.concurrent.Executor; import javax.inject.Inject; -import dagger.Lazy; - /** * {@link NotificationContentInflater} binds content to a {@link ExpandableNotificationRow} by * asynchronously building the content's {@link RemoteViews} and applying it to the row. @@ -76,27 +72,24 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final boolean mIsMediaInQS; private final NotificationRemoteInputManager mRemoteInputManager; private final NotifRemoteViewCache mRemoteViewCache; - private final Lazy<SmartReplyConstants> mSmartReplyConstants; - private final Lazy<SmartReplyController> mSmartReplyController; private final ConversationNotificationProcessor mConversationProcessor; private final Executor mBgExecutor; + private final SmartRepliesAndActionsInflater mSmartRepliesAndActionsInflater; @Inject NotificationContentInflater( NotifRemoteViewCache remoteViewCache, NotificationRemoteInputManager remoteInputManager, - Lazy<SmartReplyConstants> smartReplyConstants, - Lazy<SmartReplyController> smartReplyController, ConversationNotificationProcessor conversationProcessor, MediaFeatureFlag mediaFeatureFlag, - @Background Executor bgExecutor) { + @Background Executor bgExecutor, + SmartRepliesAndActionsInflater smartRepliesInflater) { mRemoteViewCache = remoteViewCache; mRemoteInputManager = remoteInputManager; - mSmartReplyConstants = smartReplyConstants; - mSmartReplyController = smartReplyController; mConversationProcessor = conversationProcessor; mIsMediaInQS = mediaFeatureFlag.getEnabled(); mBgExecutor = bgExecutor; + mSmartRepliesAndActionsInflater = smartRepliesInflater; } @Override @@ -132,8 +125,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder contentToBind, mRemoteViewCache, entry, - mSmartReplyConstants.get(), - mSmartReplyController.get(), mConversationProcessor, row, bindParams.isLowPriority, @@ -141,7 +132,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder bindParams.usesIncreasedHeadsUpHeight, callback, mRemoteInputManager.getRemoteViewsOnClickHandler(), - mIsMediaInQS); + mIsMediaInQS, + mSmartRepliesAndActionsInflater); if (mInflateSynchronously) { task.onPostExecute(task.doInBackground()); } else { @@ -157,17 +149,19 @@ public class NotificationContentInflater implements NotificationRowContentBinder boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, - Context packageContext) { + Context packageContext, + SmartRepliesAndActionsInflater smartRepliesInflater) { InflationProgress result = createRemoteViews(reInflateFlags, builder, bindParams.isLowPriority, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, packageContext); + result = inflateSmartReplyViews(result, reInflateFlags, entry, - row.getContext(), packageContext, row.getHeadsUpManager(), - mSmartReplyConstants.get(), mSmartReplyController.get(), - row.getExistingSmartRepliesAndActions()); + row.getContext(), packageContext, + row.getExistingSmartRepliesAndActions(), + smartRepliesInflater); apply( mBgExecutor, @@ -268,22 +262,21 @@ public class NotificationContentInflater implements NotificationRowContentBinder } } - private static InflationProgress inflateSmartReplyViews(InflationProgress result, - @InflationFlag int reInflateFlags, NotificationEntry entry, Context context, - Context packageContext, HeadsUpManager headsUpManager, - SmartReplyConstants smartReplyConstants, SmartReplyController smartReplyController, - SmartRepliesAndActions previousSmartRepliesAndActions) { + private static InflationProgress inflateSmartReplyViews( + InflationProgress result, + @InflationFlag int reInflateFlags, + NotificationEntry entry, + Context context, + Context packageContext, + SmartRepliesAndActions previousSmartRepliesAndActions, + SmartRepliesAndActionsInflater inflater) { if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null) { - result.expandedInflatedSmartReplies = - InflatedSmartReplies.inflate( - context, packageContext, entry, smartReplyConstants, - smartReplyController, headsUpManager, previousSmartRepliesAndActions); + result.expandedInflatedSmartReplies = inflater.inflateSmartReplies( + context, packageContext, entry, previousSmartRepliesAndActions); } if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null) { - result.headsUpInflatedSmartReplies = - InflatedSmartReplies.inflate( - context, packageContext, entry, smartReplyConstants, - smartReplyController, headsUpManager, previousSmartRepliesAndActions); + result.headsUpInflatedSmartReplies = inflater.inflateSmartReplies( + context, packageContext, entry, previousSmartRepliesAndActions); } return result; } @@ -709,8 +702,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final boolean mUsesIncreasedHeadsUpHeight; private final @InflationFlag int mReInflateFlags; private final NotifRemoteViewCache mRemoteViewCache; - private final SmartReplyConstants mSmartReplyConstants; - private final SmartReplyController mSmartReplyController; private final Executor mBgExecutor; private ExpandableNotificationRow mRow; private Exception mError; @@ -718,6 +709,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private CancellationSignal mCancellationSignal; private final ConversationNotificationProcessor mConversationProcessor; private final boolean mIsMediaInQS; + private final SmartRepliesAndActionsInflater mSmartRepliesInflater; private AsyncInflationTask( Executor bgExecutor, @@ -725,8 +717,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder @InflationFlag int reInflateFlags, NotifRemoteViewCache cache, NotificationEntry entry, - SmartReplyConstants smartReplyConstants, - SmartReplyController smartReplyController, ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, boolean isLowPriority, @@ -734,15 +724,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder boolean usesIncreasedHeadsUpHeight, InflationCallback callback, RemoteViews.OnClickHandler remoteViewClickHandler, - boolean isMediaFlagEnabled) { + boolean isMediaFlagEnabled, + SmartRepliesAndActionsInflater smartRepliesInflater) { mEntry = entry; mRow = row; - mSmartReplyConstants = smartReplyConstants; - mSmartReplyController = smartReplyController; mBgExecutor = bgExecutor; mInflateSynchronously = inflateSynchronously; mReInflateFlags = reInflateFlags; mRemoteViewCache = cache; + mSmartRepliesInflater = smartRepliesInflater; mContext = mRow.getContext(); mIsLowPriority = isLowPriority; mUsesIncreasedHeight = usesIncreasedHeight; @@ -786,10 +776,16 @@ public class NotificationContentInflater implements NotificationRowContentBinder InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext); - return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mEntry, - mRow.getContext(), packageContext, mRow.getHeadsUpManager(), - mSmartReplyConstants, mSmartReplyController, - mRow.getExistingSmartRepliesAndActions()); + SmartRepliesAndActions repliesAndActions = + mRow.getExistingSmartRepliesAndActions(); + return inflateSmartReplyViews( + inflationProgress, + mReInflateFlags, + mEntry, + mContext, + packageContext, + repliesAndActions, + mSmartRepliesInflater); } catch (Exception e) { mError = e; return null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 1de9308a40b1..8a644ed4d3ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewW import com.android.systemui.statusbar.policy.InflatedSmartReplies; import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions; import com.android.systemui.statusbar.policy.RemoteInputView; +import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflaterKt; import com.android.systemui.statusbar.policy.SmartReplyConstants; import com.android.systemui.statusbar.policy.SmartReplyView; @@ -1191,23 +1192,31 @@ public class NotificationContentView extends FrameLayout { View bigContentView = mExpandedChild; if (bigContentView != null && (bigContentView instanceof ViewGroup)) { - mMediaTransferManager.applyMediaTransferView((ViewGroup) bigContentView, - entry); + mMediaTransferManager.applyMediaTransferView((ViewGroup) bigContentView, entry); } View smallContentView = mContractedChild; if (smallContentView != null && (smallContentView instanceof ViewGroup)) { - mMediaTransferManager.applyMediaTransferView((ViewGroup) smallContentView, - entry); + mMediaTransferManager.applyMediaTransferView((ViewGroup) smallContentView, entry); } } + /** + * Returns whether the {@link Notification} represented by entry has a free-form remote input. + * Such an input can be used e.g. to implement smart reply buttons - by passing the replies + * through the remote input. + */ + public static boolean hasFreeformRemoteInput(NotificationEntry entry) { + Notification notification = entry.getSbn().getNotification(); + return null != notification.findRemoteInputActionPair(true /* freeform */); + } + private void applyRemoteInputAndSmartReply(final NotificationEntry entry) { if (mRemoteInputController == null) { return; } - applyRemoteInput(entry, InflatedSmartReplies.hasFreeformRemoteInput(entry)); + applyRemoteInput(entry, hasFreeformRemoteInput(entry)); if (mExpandedInflatedSmartReplies == null && mHeadsUpInflatedSmartReplies == null) { if (DEBUG) { @@ -1438,7 +1447,8 @@ public class NotificationContentView extends FrameLayout { } LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate; - if (!InflatedSmartReplies.shouldShowSmartReplyView(entry, smartRepliesAndActions)) { + if (!SmartRepliesAndActionsInflaterKt + .shouldShowSmartReplyView(entry, smartRepliesAndActions)) { smartReplyContainer.setVisibility(View.GONE); return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index b19997d15664..07a4a188bc48 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -67,7 +67,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.Prefs; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.notification.NotificationChannelHelper; @@ -75,6 +75,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import java.lang.annotation.Retention; +import java.util.Optional; import javax.inject.Provider; @@ -93,7 +94,7 @@ public class NotificationConversationInfo extends LinearLayout implements private OnUserInteractionCallback mOnUserInteractionCallback; private Handler mMainHandler; private Handler mBgHandler; - private BubbleController mBubbleController; + private Optional<Bubbles> mBubblesOptional; private String mPackageName; private String mAppName; private int mAppUid; @@ -222,7 +223,7 @@ public class NotificationConversationInfo extends LinearLayout implements @Main Handler mainHandler, @Background Handler bgHandler, OnConversationSettingsClickListener onConversationSettingsClickListener, - BubbleController bubbleController) { + Optional<Bubbles> bubblesOptional) { mSelectedAction = -1; mINotificationManager = iNotificationManager; mOnUserInteractionCallback = onUserInteractionCallback; @@ -241,7 +242,7 @@ public class NotificationConversationInfo extends LinearLayout implements mIconFactory = conversationIconFactory; mUserContext = userContext; mBubbleMetadata = bubbleMetadata; - mBubbleController = bubbleController; + mBubblesOptional = bubblesOptional; mBuilderProvider = builderProvider; mMainHandler = mainHandler; mBgHandler = bgHandler; @@ -640,9 +641,11 @@ public class NotificationConversationInfo extends LinearLayout implements mINotificationManager.setBubblesAllowed(mAppPkg, mAppUid, BUBBLE_PREFERENCE_SELECTED); } - post(() -> { - mBubbleController.onUserChangedImportance(mEntry); - }); + if (mBubblesOptional.isPresent()) { + post(() -> { + mBubblesOptional.get().onUserChangedImportance(mEntry); + }); + } } mChannelToUpdate.setImportance(Math.max( mChannelToUpdate.getOriginalImportance(), IMPORTANCE_DEFAULT)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 7d418f30e4c5..373f20e6ba96 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -47,7 +47,7 @@ import com.android.settingslib.notification.ConversationIconFactory; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -70,6 +70,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.Optional; import javax.inject.Provider; @@ -116,7 +117,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx private final Lazy<StatusBar> mStatusBarLazy; private final Handler mMainHandler; private final Handler mBgHandler; - private final BubbleController mBubbleController; + private final Optional<Bubbles> mBubblesOptional; private Runnable mOpenRunnable; private final INotificationManager mNotificationManager; private final LauncherApps mLauncherApps; @@ -141,7 +142,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx UserContextProvider contextTracker, Provider<PriorityOnboardingDialogController.Builder> builderProvider, AssistantFeedbackController assistantFeedbackController, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, UiEventLogger uiEventLogger, OnUserInteractionCallback onUserInteractionCallback) { mContext = context; @@ -157,7 +158,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx mBuilderProvider = builderProvider; mChannelEditorDialogController = channelEditorDialogController; mAssistantFeedbackController = assistantFeedbackController; - mBubbleController = bubbleController; + mBubblesOptional = bubblesOptional; mUiEventLogger = uiEventLogger; mOnUserInteractionCallback = onUserInteractionCallback; } @@ -490,7 +491,7 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx mMainHandler, mBgHandler, onConversationSettingsListener, - mBubbleController); + mBubblesOptional); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt index fe70c818216e..17f326b69848 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt @@ -147,6 +147,8 @@ class NotificationConversationTemplateViewWrapper constructor( // hiding the conversationIcon will already do that via its listener. return } + } else { + conversationIconView.isForceHidden = false } super.setShelfIconVisible(visible) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 500de2d29d03..93204995c5b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters; import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_SILENT; import static com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.ANCHOR_SCROLLING; @@ -73,6 +74,7 @@ import android.widget.ScrollView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.keyguard.KeyguardSliceView; import com.android.settingslib.Utils; @@ -97,7 +99,6 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.ShadeViewRefactor; import com.android.systemui.statusbar.notification.ShadeViewRefactor.RefactorComponent; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.logging.NotificationLogger; @@ -260,6 +261,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private boolean mDismissAllInProgress; private boolean mFadeNotificationsOnDismiss; private FooterDismissListener mFooterDismissListener; + private boolean mFlingAfterUpEvent; /** * Was the scroller scrolled to the top when the down motion was observed? @@ -3789,6 +3791,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if ((Math.abs(initialVelocity) > mMinimumVelocity)) { float currentOverScrollTop = getCurrentOverScrollAmount(true); if (currentOverScrollTop == 0.0f || initialVelocity > 0) { + mFlingAfterUpEvent = true; + setFinishScrollingCallback(() -> { + mFlingAfterUpEvent = false; + InteractionJankMonitor.getInstance() + .end(CUJ_NOTIFICATION_SHADE_SCROLL_FLING); + setFinishScrollingCallback(null); + }); fling(-initialVelocity); } else { onOverScrollFling(false, initialVelocity); @@ -3840,6 +3849,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return true; } + boolean isFlingAfterUpEvent() { + return mFlingAfterUpEvent; + } + @ShadeViewRefactor(RefactorComponent.INPUT) protected boolean isInsideQsContainer(MotionEvent ev) { return ev.getY() < mQsContainer.getBottom(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 703c214ed3ac..7698133e1521 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack; import static android.service.notification.NotificationStats.DISMISSAL_SHADE; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; @@ -47,6 +48,7 @@ import android.view.WindowInsets; import android.widget.FrameLayout; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; @@ -1301,7 +1303,8 @@ public class NotificationStackScrollLayoutController { mView.clearNotifications(ROWS_GENTLE, closeShade); } - private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove, int selectedRows) { + private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove, + @SelectedRows int selectedRows) { if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { if (selectedRows == ROWS_ALL) { mNotifCollection.dismissAllNotifications( @@ -1334,6 +1337,7 @@ public class NotificationStackScrollLayoutController { } if (selectedRows == ROWS_ALL) { try { + // TODO(b/169585328): Do not clear media player notifications mIStatusBarService.onClearAllNotifications( mLockscreenUserManager.getCurrentUserId()); } catch (Exception ignored) { @@ -1556,6 +1560,14 @@ public class NotificationStackScrollLayoutController { if (ev.getActionMasked() == MotionEvent.ACTION_UP) { mView.setCheckForLeaveBehind(true); } + + // When swiping directly on the NSSL, this would only get an onTouchEvent. + // We log any touches other than down, which will be captured by onTouchEvent. + // In the intercept we only start tracing when it's not a down (otherwise that down + // would be duplicated when intercepted). + if (scrollWantsIt && ev.getActionMasked() != MotionEvent.ACTION_DOWN) { + InteractionJankMonitor.getInstance().begin(CUJ_NOTIFICATION_SHADE_SCROLL_FLING); + } return swipeWantsIt || scrollWantsIt || expandWantsIt; } @@ -1611,7 +1623,32 @@ public class NotificationStackScrollLayoutController { if (ev.getActionMasked() == MotionEvent.ACTION_UP) { mView.setCheckForLeaveBehind(true); } + traceJankOnTouchEvent(ev.getActionMasked(), scrollerWantsIt); return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt; } + + private void traceJankOnTouchEvent(int action, boolean scrollerWantsIt) { + // Handle interaction jank monitor cases. + switch (action) { + case MotionEvent.ACTION_DOWN: + if (scrollerWantsIt) { + InteractionJankMonitor.getInstance() + .begin(CUJ_NOTIFICATION_SHADE_SCROLL_FLING); + } + break; + case MotionEvent.ACTION_UP: + if (scrollerWantsIt && !mView.isFlingAfterUpEvent()) { + InteractionJankMonitor.getInstance() + .end(CUJ_NOTIFICATION_SHADE_SCROLL_FLING); + } + break; + case MotionEvent.ACTION_CANCEL: + if (scrollerWantsIt) { + InteractionJankMonitor.getInstance() + .cancel(CUJ_NOTIFICATION_SHADE_SCROLL_FLING); + } + break; + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 95edfe37fee7..d7a8202d7a4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -29,6 +29,7 @@ import com.android.systemui.R; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.FooterView; @@ -687,15 +688,27 @@ public class StackScrollAlgorithm { AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); float childrenOnTop = 0.0f; + + int topHunIndex = -1; + for (int i = 0; i < childCount; i++) { + ExpandableView child = algorithmState.visibleChildren.get(i); + if (child instanceof ActivatableNotificationView + && (child.isAboveShelf() || child.showingPulsing())) { + topHunIndex = i; + break; + } + } + for (int i = childCount - 1; i >= 0; i--) { childrenOnTop = updateChildZValue(i, childrenOnTop, - algorithmState, ambientState); + algorithmState, ambientState, i == topHunIndex); } } protected float updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState, + boolean shouldElevateHun) { ExpandableView child = algorithmState.visibleChildren.get(i); ExpandableViewState childViewState = child.getViewState(); int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); @@ -713,8 +726,7 @@ public class StackScrollAlgorithm { } childViewState.zTranslation = baseZ + childrenOnTop * zDistanceBetweenElements; - } else if (child == ambientState.getTrackedHeadsUpRow() - || (i == 0 && (child.isAboveShelf() || child.showingPulsing()))) { + } else if (shouldElevateHun) { // In case this is a new view that has never been measured before, we don't want to // elevate if we are currently expanded more then the notification int shelfHeight = ambientState.getShelf() == null ? 0 : diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index efd6767e66a7..76c5baf6e9f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -169,7 +169,8 @@ public class AutoTileManager implements UserAwareController { String setting = split[0]; String spec = split[1]; // Populate all the settings. As they may not have been added in other users - AutoAddSetting s = new AutoAddSetting(mContext, mHandler, setting, spec); + AutoAddSetting s = new AutoAddSetting( + mContext, mHandler, setting, mCurrentUser.getIdentifier(), spec); mAutoAddSettingList.add(s); } else { Log.w(TAG, "Malformed item in array: " + tile); @@ -319,8 +320,14 @@ public class AutoTileManager implements UserAwareController { private class AutoAddSetting extends SecureSetting { private final String mSpec; - AutoAddSetting(Context context, Handler handler, String setting, String tileSpec) { - super(context, handler, setting); + AutoAddSetting( + Context context, + Handler handler, + String setting, + int userId, + String tileSpec + ) { + super(context, handler, setting, userId); mSpec = tileSpec; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index b2c1900bf1dc..780e54d56821 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -458,6 +458,7 @@ public final class DozeServiceHost implements DozeHost { return; } mSuppressed = suppressed; + mDozeLog.traceDozingSuppressed(mSuppressed); for (Callback callback : mCallbacks) { callback.onDozeSuppressedChanged(suppressed); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index d7c176207f5e..bf1118253f8a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -86,8 +86,6 @@ import com.android.systemui.statusbar.policy.PreviewInflater; import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory; import com.android.systemui.tuner.TunerService; -import java.util.concurrent.Executor; - /** * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status * text. @@ -565,7 +563,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } }; if (!mKeyguardStateController.canDismissLockScreen()) { - Dependency.get(Executor.class).execute(runnable); + Dependency.get(Dependency.BACKGROUND_EXECUTOR).execute(runnable); } else { boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr) && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java index a3f14ba28dcb..1fdf631a858d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.util.MathUtils; import com.android.keyguard.KeyguardStatusView; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Interpolators; import com.android.systemui.R; @@ -122,6 +123,8 @@ public class KeyguardClockPositionAlgorithm { */ private int mUnlockedStackScrollerPadding; + private int mLockScreenMode; + /** * Refreshes the dimension values. */ @@ -171,6 +174,13 @@ public class KeyguardClockPositionAlgorithm { result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount); } + /** + * Update lock screen mode for testing different layouts + */ + public void onLockScreenModeChanged(int mode) { + mLockScreenMode = mode; + } + public float getMinStackScrollerPadding() { return mBypassEnabled ? mUnlockedStackScrollerPadding : mMinTopMargin + mKeyguardStatusHeight + mClockNotificationsMargin; @@ -185,6 +195,9 @@ public class KeyguardClockPositionAlgorithm { } private int getExpandedPreferredClockY() { + if (mLockScreenMode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL) { + return mMinTopMargin; + } return (mHasCustomClock && (!mHasVisibleNotifs || mBypassEnabled)) ? getPreferredClockY() : getExpandedClockPosition(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java index 3d51854a348c..54fb863b5de7 100755 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockIcon.java @@ -168,7 +168,7 @@ public class LockIcon extends KeyguardAffordanceView { int iconRes = isAnim ? getThemedAnimationResId(lockAnimIndex) : getIconForState(newState); if (!mDrawableCache.contains(iconRes)) { - mDrawableCache.put(iconRes, getResources().getDrawable(iconRes)); + mDrawableCache.put(iconRes, getContext().getDrawable(iconRes)); } return mDrawableCache.get(iconRes); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java index bda35fb0a48e..d1c83555c062 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java @@ -19,7 +19,7 @@ import com.android.internal.util.ContrastColorUtil; import com.android.settingslib.Utils; import com.android.systemui.Interpolators; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; @@ -40,6 +40,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Function; import javax.inject.Inject; @@ -65,7 +66,7 @@ public class NotificationIconAreaController implements private final NotificationWakeUpCoordinator mWakeUpCoordinator; private final KeyguardBypassController mBypassController; private final DozeParameters mDozeParameters; - private final BubbleController mBubbleController; + private final Optional<Bubbles> mBubblesOptional; private final StatusBarWindowController mStatusBarWindowController; private int mIconSize; @@ -114,7 +115,7 @@ public class NotificationIconAreaController implements NotificationMediaManager notificationMediaManager, NotificationListener notificationListener, DozeParameters dozeParameters, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, DemoModeController demoModeController, DarkIconDispatcher darkIconDispatcher, StatusBarWindowController statusBarWindowController) { @@ -127,7 +128,7 @@ public class NotificationIconAreaController implements mWakeUpCoordinator = wakeUpCoordinator; wakeUpCoordinator.addListener(this); mBypassController = keyguardBypassController; - mBubbleController = bubbleController; + mBubblesOptional = bubblesOptional; mDemoModeController = demoModeController; mDemoModeController.addCallback(this); mStatusBarWindowController = statusBarWindowController; @@ -298,7 +299,7 @@ public class NotificationIconAreaController implements || !entry.isPulseSuppressed())) { return false; } - if (mBubbleController.isBubbleExpanded(entry)) { + if (mBubblesOptional.isPresent() && mBubblesOptional.get().isBubbleExpanded(entry)) { return false; } return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index cd9cc0775c66..86d4ac1cb443 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone; import static android.view.View.GONE; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters; @@ -62,6 +64,7 @@ import android.widget.FrameLayout; import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.LatencyTracker; @@ -218,6 +221,11 @@ public class NotificationPanelViewController extends PanelViewController { new KeyguardUpdateMonitorCallback() { @Override + public void onLockScreenModeChanged(int mode) { + mClockPositionAlgorithm.onLockScreenModeChanged(mode); + } + + @Override public void onBiometricAuthenticated(int userId, BiometricSourceType biometricSourceType, boolean isStrongBiometric) { @@ -1139,6 +1147,7 @@ public class NotificationPanelViewController extends PanelViewController { onQsExpansionStarted(); mInitialHeightOnTouch = mQsExpansionHeight; mQsTracking = true; + traceQsJank(true /* startTracing */, false /* wasCancelled */); mNotificationStackScrollLayoutController.cancelLongPress(); } break; @@ -1170,6 +1179,7 @@ public class NotificationPanelViewController extends PanelViewController { && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { mView.getParent().requestDisallowInterceptTouchEvent(true); mQsTracking = true; + traceQsJank(true /* startTracing */, false /* wasCancelled */); onQsExpansionStarted(); notifyExpandingFinished(); mInitialHeightOnTouch = mQsExpansionHeight; @@ -1202,6 +1212,19 @@ public class NotificationPanelViewController extends PanelViewController { && x < stackScrollerX + mNotificationStackScrollLayoutController.getWidth(); } + private void traceQsJank(boolean startTracing, boolean wasCancelled) { + InteractionJankMonitor monitor = InteractionJankMonitor.getInstance(); + if (startTracing) { + monitor.begin(CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE); + } else { + if (wasCancelled) { + monitor.cancel(CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE); + } else { + monitor.end(CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE); + } + } + } + private void initDownStates(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mOnlyAffordanceInThisMotion = false; @@ -1315,9 +1338,9 @@ public class NotificationPanelViewController extends PanelViewController { final int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f && mBarState != KEYGUARD && !mQsExpanded && mQsExpansionEnabled) { - // Down in the empty area while fully expanded - go to QS. mQsTracking = true; + traceQsJank(true /* startTracing */, false /* wasCancelled */); mConflictingQsExpansionGesture = true; onQsExpansionStarted(); mInitialHeightOnTouch = mQsExpansionHeight; @@ -1405,6 +1428,7 @@ public class NotificationPanelViewController extends PanelViewController { return; } mExpectingSynthesizedDown = true; + InteractionJankMonitor.getInstance().begin(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); onTrackingStarted(); updatePanelExpanded(); } @@ -1474,6 +1498,7 @@ public class NotificationPanelViewController extends PanelViewController { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mQsTracking = true; + traceQsJank(true /* startTracing */, false /* wasCancelled */); mInitialTouchY = y; mInitialTouchX = x; onQsExpansionStarted(); @@ -1513,6 +1538,9 @@ public class NotificationPanelViewController extends PanelViewController { if (fraction != 0f || y >= mInitialTouchY) { flingQsWithCurrentVelocity(y, event.getActionMasked() == MotionEvent.ACTION_CANCEL); + } else { + traceQsJank(false /* startTracing */, + event.getActionMasked() == MotionEvent.ACTION_CANCEL); } if (mQsVelocityTracker != null) { mQsVelocityTracker.recycle(); @@ -1893,7 +1921,7 @@ public class NotificationPanelViewController extends PanelViewController { * @see #flingSettings(float, int, Runnable, boolean) */ public void flingSettings(float vel, int type) { - flingSettings(vel, type, null, false /* isClick */); + flingSettings(vel, type, null /* onFinishRunnable */, false /* isClick */); } /** @@ -1923,6 +1951,7 @@ public class NotificationPanelViewController extends PanelViewController { if (onFinishRunnable != null) { onFinishRunnable.run(); } + traceQsJank(false /* startTracing */, type != FLING_EXPAND /* wasCancelled */); return; } @@ -1947,12 +1976,18 @@ public class NotificationPanelViewController extends PanelViewController { setQsExpansion((Float) animation.getAnimatedValue()); }); animator.addListener(new AnimatorListenerAdapter() { + private boolean mIsCanceled; @Override public void onAnimationStart(Animator animation) { notifyExpandingStarted(); } @Override + public void onAnimationCancel(Animator animation) { + mIsCanceled = true; + } + + @Override public void onAnimationEnd(Animator animation) { mAnimatingQS = false; notifyExpandingFinished(); @@ -1961,6 +1996,7 @@ public class NotificationPanelViewController extends PanelViewController { if (onFinishRunnable != null) { onFinishRunnable.run(); } + traceQsJank(false /* startTracing */, mIsCanceled /* wasCancelled */); } }); // Let's note that we're animating QS. Moving the animator here will cancel it immediately, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowView.java index bc80a1a5137d..a4fc3a39c706 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowView.java @@ -49,6 +49,7 @@ import android.view.WindowInsets; import android.view.WindowInsetsController; import android.widget.FrameLayout; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.view.FloatingActionMode; import com.android.internal.widget.FloatingToolbar; import com.android.systemui.R; @@ -145,6 +146,7 @@ public class NotificationShadeWindowView extends FrameLayout { protected void onAttachedToWindow() { super.onAttachedToWindow(); setWillNotDraw(!DEBUG); + InteractionJankMonitor.getInstance().init(this); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java index 271650fabc4b..06fd6564854b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; import static com.android.systemui.classifier.Classifier.UNLOCK; @@ -40,6 +41,7 @@ import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.Interpolator; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.LatencyTracker; import com.android.systemui.DejankUtils; @@ -110,6 +112,7 @@ public abstract class PanelViewController { private boolean mMotionAborted; private boolean mUpwardsWhenThresholdReached; private boolean mAnimatingOnDown; + private boolean mHandlingPointerUp; private ValueAnimator mHeightAnimator; private ObjectAnimator mPeekAnimator; @@ -364,6 +367,9 @@ public abstract class PanelViewController { protected void startExpandMotion(float newX, float newY, boolean startTracking, float expandedHeight) { + if (!mHandlingPointerUp) { + InteractionJankMonitor.getInstance().begin(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); + } mInitialOffsetOnTouch = expandedHeight; mInitialTouchY = newY; mInitialTouchX = newX; @@ -579,6 +585,7 @@ public abstract class PanelViewController { target = getMaxPanelHeight() - getClearAllHeightWithPadding(); } if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) { + InteractionJankMonitor.getInstance().end(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); notifyExpandingFinished(); return; } @@ -640,7 +647,12 @@ public abstract class PanelViewController { } setAnimator(null); if (!mCancelled) { + InteractionJankMonitor.getInstance() + .end(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); notifyExpandingFinished(); + } else { + InteractionJankMonitor.getInstance() + .cancel(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); } notifyBarPanelExpansionChanged(); } @@ -1290,7 +1302,9 @@ public abstract class PanelViewController { final float newY = event.getY(newIndex); final float newX = event.getX(newIndex); mTrackingPointer = event.getPointerId(newIndex); + mHandlingPointerUp = true; startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight); + mHandlingPointerUp = false; } break; case MotionEvent.ACTION_POINTER_DOWN: @@ -1348,6 +1362,12 @@ public abstract class PanelViewController { case MotionEvent.ACTION_CANCEL: addMovement(event); endMotionEvent(event, x, y, false /* forceCancel */); + InteractionJankMonitor monitor = InteractionJankMonitor.getInstance(); + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + monitor.end(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); + } else { + monitor.cancel(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE); + } break; } return !mGestureWaitForTouchSlop || mTracking; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index e95cf2806691..4af27877c201 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -34,6 +34,8 @@ import android.view.ViewTreeObserver; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor; import com.android.internal.colorextraction.ColorExtractor.GradientColors; @@ -139,6 +141,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo private ScrimView mScrimInFront; private ScrimView mScrimBehind; + @Nullable private ScrimView mScrimForBubble; private Runnable mScrimBehindChangeRunnable; @@ -238,7 +241,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo * Attach the controller to the supplied views. */ public void attachViews( - ScrimView scrimBehind, ScrimView scrimInFront, ScrimView scrimForBubble) { + ScrimView scrimBehind, ScrimView scrimInFront, @Nullable ScrimView scrimForBubble) { mScrimBehind = scrimBehind; mScrimInFront = scrimInFront; mScrimForBubble = scrimForBubble; @@ -258,7 +261,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo mScrimBehind.setDefaultFocusHighlightEnabled(false); mScrimInFront.setDefaultFocusHighlightEnabled(false); - mScrimForBubble.setDefaultFocusHighlightEnabled(false); + if (mScrimForBubble != null) { + mScrimForBubble.setDefaultFocusHighlightEnabled(false); + } updateScrims(); mKeyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback); } @@ -455,7 +460,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo } } - private void setOrAdaptCurrentAnimation(View scrim) { + private void setOrAdaptCurrentAnimation(@Nullable View scrim) { + if (scrim == null) { + return; + } + float alpha = getCurrentScrimAlpha(scrim); if (isAnimating(scrim)) { // Adapt current animation. @@ -606,11 +615,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo // Only animate scrim color if the scrim view is actually visible boolean animateScrimInFront = mScrimInFront.getViewAlpha() != 0 && !mBlankScreen; boolean animateScrimBehind = mScrimBehind.getViewAlpha() != 0 && !mBlankScreen; - boolean animateScrimForBubble = mScrimForBubble.getViewAlpha() != 0 && !mBlankScreen; mScrimInFront.setColors(mColors, animateScrimInFront); mScrimBehind.setColors(mColors, animateScrimBehind); - mScrimForBubble.setColors(mColors, animateScrimForBubble); // Calculate minimum scrim opacity for white or black text. int textColor = mColors.supportsDarkText() ? Color.BLACK : Color.WHITE; @@ -632,7 +639,12 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo } setScrimAlpha(mScrimInFront, mInFrontAlpha); setScrimAlpha(mScrimBehind, mBehindAlpha); - setScrimAlpha(mScrimForBubble, mBubbleAlpha); + + if (mScrimForBubble != null) { + boolean animateScrimForBubble = mScrimForBubble.getViewAlpha() != 0 && !mBlankScreen; + mScrimForBubble.setColors(mColors, animateScrimForBubble); + setScrimAlpha(mScrimForBubble, mBubbleAlpha); + } // The animation could have all already finished, let's call onFinished just in case onFinished(); dispatchScrimsVisible(); @@ -828,12 +840,14 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo mBubbleTint = Color.TRANSPARENT; updateScrimColor(mScrimInFront, mInFrontAlpha, mInFrontTint); updateScrimColor(mScrimBehind, mBehindAlpha, mBehindTint); - updateScrimColor(mScrimForBubble, mBubbleAlpha, mBubbleTint); + if (mScrimForBubble != null) { + updateScrimColor(mScrimForBubble, mBubbleAlpha, mBubbleTint); + } } } - private boolean isAnimating(View scrim) { - return scrim.getTag(TAG_KEY_ANIM) != null; + private boolean isAnimating(@Nullable View scrim) { + return scrim != null && scrim.getTag(TAG_KEY_ANIM) != null; } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 2db36f4a62f8..fc91c16f1a48 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.phone; import android.graphics.Color; import android.os.Trace; +import androidx.annotation.Nullable; + import com.android.systemui.dock.DockManager; import com.android.systemui.statusbar.ScrimView; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; @@ -212,7 +214,9 @@ public enum ScrimState { // Set all scrims black, before they fade transparent. updateScrimColor(mScrimInFront, 1f /* alpha */, Color.BLACK /* tint */); updateScrimColor(mScrimBehind, 1f /* alpha */, Color.BLACK /* tint */); - updateScrimColor(mScrimForBubble, 1f /* alpha */, Color.BLACK /* tint */); + if (mScrimForBubble != null) { + updateScrimColor(mScrimForBubble, 1f /* alpha */, Color.BLACK /* tint */); + } // Scrims should still be black at the end of the transition. mFrontTint = Color.BLACK; @@ -258,7 +262,7 @@ public enum ScrimState { float mDefaultScrimAlpha; ScrimView mScrimInFront; ScrimView mScrimBehind; - ScrimView mScrimForBubble; + @Nullable ScrimView mScrimForBubble; DozeParameters mDozeParameters; DockManager mDockManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java index 1ce22194878f..af2f3e55c9ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java @@ -22,7 +22,7 @@ import android.view.ViewTreeObserver; import android.view.WindowManager; import com.android.systemui.assist.AssistManager; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import java.util.ArrayList; +import java.util.Optional; import javax.inject.Inject; @@ -50,7 +51,7 @@ public class ShadeControllerImpl implements ShadeController { private final int mDisplayId; protected final Lazy<StatusBar> mStatusBarLazy; private final Lazy<AssistManager> mAssistManagerLazy; - private final Lazy<BubbleController> mBubbleControllerLazy; + private final Optional<Lazy<Bubbles>> mBubblesOptional; private final ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>(); @@ -63,7 +64,7 @@ public class ShadeControllerImpl implements ShadeController { WindowManager windowManager, Lazy<StatusBar> statusBarLazy, Lazy<AssistManager> assistManagerLazy, - Lazy<BubbleController> bubbleControllerLazy + Optional<Lazy<Bubbles>> bubblesOptional ) { mCommandQueue = commandQueue; mStatusBarStateController = statusBarStateController; @@ -73,7 +74,7 @@ public class ShadeControllerImpl implements ShadeController { // TODO: Remove circular reference to StatusBar when possible. mStatusBarLazy = statusBarLazy; mAssistManagerLazy = assistManagerLazy; - mBubbleControllerLazy = bubbleControllerLazy; + mBubblesOptional = bubblesOptional; } @Override @@ -133,8 +134,8 @@ public class ShadeControllerImpl implements ShadeController { getStatusBar().getNotificationShadeWindowViewController().cancelExpandHelper(); getStatusBarView().collapsePanel(true /* animate */, delayed, speedUpFactor); - } else { - mBubbleControllerLazy.get().collapseStack(); + } else if (mBubblesOptional.isPresent()) { + mBubblesOptional.get().get().collapseStack(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 8d5e05f8a894..28a22cd6a194 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.phone; -import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT; -import static android.app.ActivityTaskManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.StatusBarManager.WindowType; @@ -37,7 +35,6 @@ import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASL import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING; import static com.android.systemui.shared.system.WindowManagerWrapper.NAV_BAR_POS_INVALID; -import static com.android.systemui.shared.system.WindowManagerWrapper.NAV_BAR_POS_LEFT; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT; @@ -72,6 +69,7 @@ import android.content.IntentFilter; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.PointF; @@ -147,6 +145,7 @@ import com.android.systemui.SystemUI; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.charging.WirelessChargingAnimation; import com.android.systemui.classifier.FalsingLog; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -154,6 +153,7 @@ import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeCommandReceiver; import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.emergency.EmergencyGesture; import com.android.systemui.fragments.ExtensionFragmentListener; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.keyguard.DismissCallbackRegistry; @@ -173,7 +173,6 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSFragment; import com.android.systemui.qs.QSPanel; -import com.android.systemui.recents.Recents; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.system.WindowManagerWrapper; @@ -649,7 +648,7 @@ public class StatusBar extends SystemUI implements DemoMode, protected StatusBarNotificationPresenter mPresenter; private NotificationActivityStarter mNotificationActivityStarter; private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy; - private final BubbleController mBubbleController; + private final Optional<Bubbles> mBubblesOptional; private final BubbleController.BubbleExpandListener mBubbleExpandListener; private ActivityIntentHelper mActivityIntentHelper; @@ -698,7 +697,7 @@ public class StatusBar extends SystemUI implements DemoMode, WakefulnessLifecycle wakefulnessLifecycle, SysuiStatusBarStateController statusBarStateController, VibratorHelper vibratorHelper, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, VisualStabilityManager visualStabilityManager, DeviceProvisionedController deviceProvisionedController, NavigationBarController navigationBarController, @@ -717,7 +716,6 @@ public class StatusBar extends SystemUI implements DemoMode, DozeScrimController dozeScrimController, VolumeComponent volumeComponent, CommandQueue commandQueue, - Optional<Recents> recentsOptional, Provider<StatusBarComponent.Builder> statusBarComponentBuilder, PluginManager pluginManager, Optional<SplitScreen> splitScreenOptional, @@ -778,7 +776,7 @@ public class StatusBar extends SystemUI implements DemoMode, mWakefulnessLifecycle = wakefulnessLifecycle; mStatusBarStateController = statusBarStateController; mVibratorHelper = vibratorHelper; - mBubbleController = bubbleController; + mBubblesOptional = bubblesOptional; mVisualStabilityManager = visualStabilityManager; mDeviceProvisionedController = deviceProvisionedController; mNavigationBarController = navigationBarController; @@ -798,7 +796,6 @@ public class StatusBar extends SystemUI implements DemoMode, mNotificationShadeDepthControllerLazy = notificationShadeDepthControllerLazy; mVolumeComponent = volumeComponent; mCommandQueue = commandQueue; - mRecentsOptional = recentsOptional; mStatusBarComponentBuilder = statusBarComponentBuilder; mPluginManager = pluginManager; mSplitScreenOptional = splitScreenOptional; @@ -824,7 +821,7 @@ public class StatusBar extends SystemUI implements DemoMode, updateScrimController(); }; - + mActivityIntentHelper = new ActivityIntentHelper(mContext); DateTimeView.setReceiverHandler(timeTickHandler); } @@ -834,8 +831,9 @@ public class StatusBar extends SystemUI implements DemoMode, mWakefulnessLifecycle.addObserver(mWakefulnessObserver); mUiModeManager = mContext.getSystemService(UiModeManager.class); mBypassHeadsUpNotifier.setUp(); - mBubbleController.setExpandListener(mBubbleExpandListener); - mActivityIntentHelper = new ActivityIntentHelper(mContext); + if (mBubblesOptional.isPresent()) { + mBubblesOptional.get().setExpandListener(mBubbleExpandListener); + } mColorExtractor.addOnColorsChangedListener(this); mStatusBarStateController.addCallback(this, @@ -1145,7 +1143,8 @@ public class StatusBar extends SystemUI implements DemoMode, ScrimView scrimBehind = mNotificationShadeWindowView.findViewById(R.id.scrim_behind); ScrimView scrimInFront = mNotificationShadeWindowView.findViewById(R.id.scrim_in_front); - ScrimView scrimForBubble = mBubbleController.getScrimForBubble(); + ScrimView scrimForBubble = mBubblesOptional.isPresent() + ? mBubblesOptional.get().getScrimForBubble() : null; mScrimController.setScrimVisibleListener(scrimsVisible -> { mNotificationShadeWindowController.setScrimsVisibility(scrimsVisible); @@ -1341,6 +1340,7 @@ public class StatusBar extends SystemUI implements DemoMode, mNotificationsController.initialize( this, + mBubblesOptional, mPresenter, mStackScrollerController.getNotificationListContainer(), mNotificationActivityStarter, @@ -1542,35 +1542,37 @@ public class StatusBar extends SystemUI implements DemoMode, } public boolean toggleSplitScreenMode(int metricsDockAction, int metricsUndockAction) { - if (!mRecentsOptional.isPresent()) { + if (!mSplitScreenOptional.isPresent()) { return false; } - if (mSplitScreenOptional.isPresent()) { - SplitScreen splitScreen = mSplitScreenOptional.get(); - if (splitScreen.isDividerVisible()) { - if (splitScreen.isMinimized() - && !splitScreen.isHomeStackResizable()) { - // Undocking from the minimized state is not supported - return false; - } else { - splitScreen.onUndockingTask(); - if (metricsUndockAction != -1) { - mMetricsLogger.action(metricsUndockAction); - } - } - return true; + final SplitScreen splitScreen = mSplitScreenOptional.get(); + if (splitScreen.isDividerVisible()) { + if (splitScreen.isMinimized() && !splitScreen.isHomeStackResizable()) { + // Undocking from the minimized state is not supported + return false; + } + + splitScreen.onUndockingTask(); + if (metricsUndockAction != -1) { + mMetricsLogger.action(metricsUndockAction); } + return true; } final int navbarPos = WindowManagerWrapper.getInstance().getNavBarPosition(mDisplayId); if (navbarPos == NAV_BAR_POS_INVALID) { return false; } - int createMode = navbarPos == NAV_BAR_POS_LEFT - ? SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT - : SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT; - return mRecentsOptional.get().splitPrimaryTask(createMode, null, metricsDockAction); + + if (splitScreen.splitPrimaryTask()) { + if (metricsDockAction != -1) { + mMetricsLogger.action(metricsDockAction); + } + return true; + } + + return false; } /** @@ -2491,10 +2493,12 @@ public class StatusBar extends SystemUI implements DemoMode, /** Temporarily hides Bubbles if the status bar is hidden. */ private void updateBubblesVisibility() { - mBubbleController.onStatusBarVisibilityChanged( - mStatusBarMode != MODE_LIGHTS_OUT - && mStatusBarMode != MODE_LIGHTS_OUT_TRANSPARENT - && !mStatusBarWindowHidden); + if (mBubblesOptional.isPresent()) { + mBubblesOptional.get().onStatusBarVisibilityChanged( + mStatusBarMode != MODE_LIGHTS_OUT + && mStatusBarMode != MODE_LIGHTS_OUT_TRANSPARENT + && !mStatusBarWindowHidden); + } } void checkBarMode(@TransitionMode int mode, @WindowVisibleState int windowState, @@ -2817,8 +2821,8 @@ public class StatusBar extends SystemUI implements DemoMode, if (mRemoteInputManager.getController() != null) { mRemoteInputManager.getController().closeRemoteInputs(); } - if (mBubbleController.isStackExpanded()) { - mBubbleController.collapseStack(); + if (mBubblesOptional.isPresent() && mBubblesOptional.get().isStackExpanded()) { + mBubblesOptional.get().collapseStack(); } if (mLockscreenUserManager.isCurrentProfile(getSendingUserId())) { int flags = CommandQueue.FLAG_EXCLUDE_NONE; @@ -2833,9 +2837,9 @@ public class StatusBar extends SystemUI implements DemoMode, if (mNotificationShadeWindowController != null) { mNotificationShadeWindowController.setNotTouchable(false); } - if (mBubbleController.isStackExpanded()) { + if (mBubblesOptional.isPresent() && mBubblesOptional.get().isStackExpanded()) { // Post to main thread handler, since updating the UI. - mMainThreadHandler.post(() -> mBubbleController.collapseStack()); + mMainThreadHandler.post(() -> mBubblesOptional.get().collapseStack()); } finishBarAnimations(); resetUserExpandedStates(); @@ -3535,8 +3539,6 @@ public class StatusBar extends SystemUI implements DemoMode, if (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED) { if (mNotificationPanelViewController.canPanelBeCollapsed()) { mShadeController.animateCollapsePanels(); - } else { - mBubbleController.performBackPressIfNeeded(); } return true; } @@ -3979,6 +3981,27 @@ public class StatusBar extends SystemUI implements DemoMode, } } + @Override + public void onEmergencyActionLaunchGestureDetected() { + // TODO (b/169793384) Polish the panic gesture to be just like its older brother, camera. + Intent emergencyIntent = new Intent(EmergencyGesture.ACTION_LAUNCH_EMERGENCY); + PackageManager pm = mContext.getPackageManager(); + ResolveInfo resolveInfo = pm.resolveActivity(emergencyIntent, /*flags=*/0); + if (resolveInfo == null) { + Log.wtf(TAG, "Couldn't find an app to process the emergency intent."); + return; + } + + if (mVibrator != null && mVibrator.hasVibrator()) { + mVibrator.vibrate(500L); + } + + emergencyIntent.setComponent(new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name)); + emergencyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(emergencyIntent, /*dismissShade=*/true); + } + boolean isCameraAllowedByAdmin() { if (mDevicePolicyManager.getCameraDisabled(null, mLockscreenUserManager.getCurrentUserId())) { @@ -4055,7 +4078,7 @@ public class StatusBar extends SystemUI implements DemoMode, mScrimController.transitionTo(ScrimState.AOD); } else if (mIsKeyguard && !unlocking) { mScrimController.transitionTo(ScrimState.KEYGUARD); - } else if (mBubbleController.isStackExpanded()) { + } else if (mBubblesOptional.isPresent() && mBubblesOptional.get().isStackExpanded()) { mScrimController.transitionTo(ScrimState.BUBBLE_EXPANDED, mUnlockScrimCallback); } else { mScrimController.transitionTo(ScrimState.UNLOCKED, mUnlockScrimCallback); @@ -4114,8 +4137,6 @@ public class StatusBar extends SystemUI implements DemoMode, protected Display mDisplay; private int mDisplayId; - private final Optional<Recents> mRecentsOptional; - protected NotificationShelfController mNotificationShelfController; private final Lazy<AssistManager> mAssistManagerLazy; @@ -4283,6 +4304,19 @@ public class StatusBar extends SystemUI implements DemoMode, } public static Bundle getActivityOptions(@Nullable RemoteAnimationAdapter animationAdapter) { + return getDefaultActivityOptions(animationAdapter).toBundle(); + } + + public static Bundle getActivityOptions(@Nullable RemoteAnimationAdapter animationAdapter, + boolean isKeyguardShowing, long eventTime) { + ActivityOptions options = getDefaultActivityOptions(animationAdapter); + options.setSourceInfo(isKeyguardShowing ? ActivityOptions.SourceInfo.TYPE_LOCKSCREEN + : ActivityOptions.SourceInfo.TYPE_NOTIFICATION, eventTime); + return options.toBundle(); + } + + public static ActivityOptions getDefaultActivityOptions( + @Nullable RemoteAnimationAdapter animationAdapter) { ActivityOptions options; if (animationAdapter != null) { options = ActivityOptions.makeRemoteAnimation(animationAdapter); @@ -4292,7 +4326,7 @@ public class StatusBar extends SystemUI implements DemoMode, // Anything launched from the notification shade should always go into the secondary // split-screen windowing mode. options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY); - return options.toBundle(); + return options; } void visibilityChanged(boolean visible) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 737cdeba797a..256ee2081f41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -30,6 +30,7 @@ import android.app.TaskStackBuilder; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; @@ -40,7 +41,6 @@ import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.EventLog; import android.view.RemoteAnimationAdapter; -import android.view.View; import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.NotificationVisibility; @@ -48,7 +48,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.EventLogTags; import com.android.systemui.assist.AssistManager; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -77,6 +77,7 @@ import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.Optional; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -103,7 +104,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final KeyguardManager mKeyguardManager; private final IDreamManager mDreamManager; - private final BubbleController mBubbleController; + private final Optional<Bubbles> mBubblesOptional; private final Lazy<AssistManager> mAssistManagerLazy; private final NotificationRemoteInputManager mRemoteInputManager; private final GroupMembershipManager mGroupMembershipManager; @@ -141,7 +142,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit StatusBarKeyguardViewManager statusBarKeyguardViewManager, KeyguardManager keyguardManager, IDreamManager dreamManager, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, Lazy<AssistManager> assistManagerLazy, NotificationRemoteInputManager remoteInputManager, GroupMembershipManager groupMembershipManager, @@ -175,7 +176,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; - mBubbleController = bubbleController; + mBubblesOptional = bubblesOptional; mAssistManagerLazy = assistManagerLazy; mRemoteInputManager = remoteInputManager; mGroupMembershipManager = groupMembershipManager; @@ -386,11 +387,14 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } private void expandBubbleStackOnMainThread(NotificationEntry entry) { + if (!mBubblesOptional.isPresent()) { + return; + } + if (Looper.getMainLooper().isCurrentThread()) { - mBubbleController.expandStackAndSelectBubble(entry); + mBubblesOptional.get().expandStackAndSelectBubble(entry); } else { - mMainThreadHandler.post( - () -> mBubbleController.expandStackAndSelectBubble(entry)); + mMainThreadHandler.post(() -> mBubblesOptional.get().expandStackAndSelectBubble(entry)); } } @@ -398,7 +402,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit PendingIntent intent, Intent fillInIntent, NotificationEntry entry, - View row, + ExpandableNotificationRow row, boolean wasOccluded, boolean isActivityIntent) { RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation(row, @@ -410,8 +414,11 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit .registerRemoteAnimationForNextActivityStart( intent.getCreatorPackage(), adapter); } + long eventTime = row.getAndResetLastActionUpTime(); + Bundle options = eventTime > 0 ? getActivityOptions(adapter, + mKeyguardStateController.isShowing(), eventTime) : getActivityOptions(adapter); int launchResult = intent.sendAndReturnResult(mContext, 0, fillInIntent, null, - null, null, getActivityOptions(adapter)); + null, null, options); mMainThreadHandler.post(() -> { mActivityLaunchAnimator.setLaunchResult(launchResult, isActivityIntent); }); @@ -602,7 +609,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final KeyguardManager mKeyguardManager; private final IDreamManager mDreamManager; - private final BubbleController mBubbleController; + private final Optional<Bubbles> mBubblesOptional; private final Lazy<AssistManager> mAssistManagerLazy; private final NotificationRemoteInputManager mRemoteInputManager; private final GroupMembershipManager mGroupMembershipManager; @@ -639,7 +646,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit StatusBarKeyguardViewManager statusBarKeyguardViewManager, KeyguardManager keyguardManager, IDreamManager dreamManager, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, Lazy<AssistManager> assistManagerLazy, NotificationRemoteInputManager remoteInputManager, GroupMembershipManager groupMembershipManager, @@ -669,7 +676,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; - mBubbleController = bubbleController; + mBubblesOptional = bubblesOptional; mAssistManagerLazy = assistManagerLazy; mRemoteInputManager = remoteInputManager; mGroupMembershipManager = groupMembershipManager; @@ -725,7 +732,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mStatusBarKeyguardViewManager, mKeyguardManager, mDreamManager, - mBubbleController, + mBubblesOptional, mAssistManagerLazy, mRemoteInputManager, mGroupMembershipManager, @@ -736,12 +743,10 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mLockPatternUtils, mRemoteInputCallback, mActivityIntentHelper, - mFeatureFlags, mMetricsLogger, mLogger, mOnUserInteractionCallback, - mStatusBar, mNotificationPresenter, mNotificationPanelViewController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index b7f83145f477..6d4099b656cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -31,7 +31,7 @@ import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.InitController; import com.android.systemui.assist.AssistManager; import com.android.systemui.broadcast.BroadcastDispatcher; -import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -43,7 +43,6 @@ import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginDependencyProvider; -import com.android.systemui.recents.Recents; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.CommandQueue; @@ -156,7 +155,7 @@ public interface StatusBarPhoneModule { WakefulnessLifecycle wakefulnessLifecycle, SysuiStatusBarStateController statusBarStateController, VibratorHelper vibratorHelper, - BubbleController bubbleController, + Optional<Bubbles> bubblesOptional, VisualStabilityManager visualStabilityManager, DeviceProvisionedController deviceProvisionedController, NavigationBarController navigationBarController, @@ -175,7 +174,6 @@ public interface StatusBarPhoneModule { DozeScrimController dozeScrimController, VolumeComponent volumeComponent, CommandQueue commandQueue, - Optional<Recents> recentsOptional, Provider<StatusBarComponent.Builder> statusBarComponentBuilder, PluginManager pluginManager, Optional<SplitScreen> splitScreenOptional, @@ -235,7 +233,7 @@ public interface StatusBarPhoneModule { wakefulnessLifecycle, statusBarStateController, vibratorHelper, - bubbleController, + bubblesOptional, visualStabilityManager, deviceProvisionedController, navigationBarController, @@ -254,7 +252,6 @@ public interface StatusBarPhoneModule { dozeScrimController, volumeComponent, commandQueue, - recentsOptional, statusBarComponentBuilder, pluginManager, splitScreenOptional, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index c43ad36d4462..82ad00ad7c6d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -20,6 +20,7 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Notification; import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; @@ -359,6 +360,11 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { return false; } + private static boolean isOngoingCallNotif(NotificationEntry entry) { + return entry.getSbn().isOngoing() && Notification.CATEGORY_CALL.equals( + entry.getSbn().getNotification().category); + } + /** * This represents a notification and how long it is in a heads up mode. It also manages its * lifecycle automatically when created. @@ -391,6 +397,15 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { return 1; } + boolean selfCall = isOngoingCallNotif(mEntry); + boolean otherCall = isOngoingCallNotif(headsUpEntry.mEntry); + + if (selfCall && !otherCall) { + return -1; + } else if (!selfCall && otherCall) { + return 1; + } + if (remoteInputActive && !headsUpEntry.remoteInputActive) { return -1; } else if (!remoteInputActive && headsUpEntry.remoteInputActive) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java index c6ae669d5d08..cbc8405cc057 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java @@ -19,27 +19,8 @@ package com.android.systemui.statusbar.policy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; -import android.app.RemoteInput; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.os.Build; -import android.util.Log; -import android.util.Pair; import android.widget.Button; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.ArrayUtils; -import com.android.systemui.Dependency; -import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.DevicePolicyManagerWrapper; -import com.android.systemui.shared.system.PackageManagerWrapper; -import com.android.systemui.statusbar.NotificationUiAdjustment; -import com.android.systemui.statusbar.SmartReplyController; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; - -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -48,13 +29,11 @@ import java.util.List; * thread, to later be accessed and modified on the (performance critical) UI thread. */ public class InflatedSmartReplies { - private static final String TAG = "InflatedSmartReplies"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @Nullable private final SmartReplyView mSmartReplyView; @Nullable private final List<Button> mSmartSuggestionButtons; @NonNull private final SmartRepliesAndActions mSmartRepliesAndActions; - private InflatedSmartReplies( + public InflatedSmartReplies( @Nullable SmartReplyView smartReplyView, @Nullable List<Button> smartSuggestionButtons, @NonNull SmartRepliesAndActions smartRepliesAndActions) { @@ -76,206 +55,6 @@ public class InflatedSmartReplies { } /** - * Inflate a SmartReplyView and its smart suggestions. - */ - public static InflatedSmartReplies inflate( - Context context, - Context packageContext, - NotificationEntry entry, - SmartReplyConstants smartReplyConstants, - SmartReplyController smartReplyController, - HeadsUpManager headsUpManager, - SmartRepliesAndActions existingSmartRepliesAndActions) { - SmartRepliesAndActions newSmartRepliesAndActions = - chooseSmartRepliesAndActions(smartReplyConstants, entry); - if (!shouldShowSmartReplyView(entry, newSmartRepliesAndActions)) { - return new InflatedSmartReplies(null /* smartReplyView */, - null /* smartSuggestionButtons */, newSmartRepliesAndActions); - } - - // Only block clicks if the smart buttons are different from the previous set - to avoid - // scenarios where a user incorrectly cannot click smart buttons because the notification is - // updated. - boolean delayOnClickListener = - !areSuggestionsSimilar(existingSmartRepliesAndActions, newSmartRepliesAndActions); - - SmartReplyView smartReplyView = SmartReplyView.inflate(context); - - List<Button> suggestionButtons = new ArrayList<>(); - if (newSmartRepliesAndActions.smartReplies != null) { - suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput( - newSmartRepliesAndActions.smartReplies, smartReplyController, entry, - delayOnClickListener)); - } - if (newSmartRepliesAndActions.smartActions != null) { - suggestionButtons.addAll( - smartReplyView.inflateSmartActions(packageContext, - newSmartRepliesAndActions.smartActions, smartReplyController, entry, - headsUpManager, delayOnClickListener)); - } - - return new InflatedSmartReplies(smartReplyView, suggestionButtons, - newSmartRepliesAndActions); - } - - @VisibleForTesting - static boolean areSuggestionsSimilar( - SmartRepliesAndActions left, SmartRepliesAndActions right) { - if (left == right) return true; - if (left == null || right == null) return false; - - if (!left.getSmartReplies().equals(right.getSmartReplies())) { - return false; - } - - return !NotificationUiAdjustment.areDifferent( - left.getSmartActions(), right.getSmartActions()); - } - - /** - * Returns whether we should show the smart reply view and its smart suggestions. - */ - public static boolean shouldShowSmartReplyView( - NotificationEntry entry, - SmartRepliesAndActions smartRepliesAndActions) { - if (smartRepliesAndActions.smartReplies == null - && smartRepliesAndActions.smartActions == null) { - // There are no smart replies and no smart actions. - return false; - } - // If we are showing the spinner we don't want to add the buttons. - boolean showingSpinner = entry.getSbn().getNotification() - .extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); - if (showingSpinner) { - return false; - } - // If we are keeping the notification around while sending we don't want to add the buttons. - boolean hideSmartReplies = entry.getSbn().getNotification() - .extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false); - if (hideSmartReplies) { - return false; - } - return true; - } - - /** - * Chose what smart replies and smart actions to display. App generated suggestions take - * precedence. So if the app provides any smart replies, we don't show any - * replies or actions generated by the NotificationAssistantService (NAS), and if the app - * provides any smart actions we also don't show any NAS-generated replies or actions. - */ - @NonNull - public static SmartRepliesAndActions chooseSmartRepliesAndActions( - SmartReplyConstants smartReplyConstants, - final NotificationEntry entry) { - Notification notification = entry.getSbn().getNotification(); - Pair<RemoteInput, Notification.Action> remoteInputActionPair = - notification.findRemoteInputActionPair(false /* freeform */); - Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair = - notification.findRemoteInputActionPair(true /* freeform */); - - if (!smartReplyConstants.isEnabled()) { - if (DEBUG) { - Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for " - + entry.getSbn().getKey()); - } - return new SmartRepliesAndActions(null, null); - } - // Only use smart replies from the app if they target P or above. We have this check because - // the smart reply API has been used for other things (Wearables) in the past. The API to - // add smart actions is new in Q so it doesn't require a target-sdk check. - boolean enableAppGeneratedSmartReplies = (!smartReplyConstants.requiresTargetingP() - || entry.targetSdk >= Build.VERSION_CODES.P); - - boolean appGeneratedSmartRepliesExist = - enableAppGeneratedSmartReplies - && remoteInputActionPair != null - && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices()) - && remoteInputActionPair.second.actionIntent != null; - - List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions(); - boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty(); - - SmartReplyView.SmartReplies smartReplies = null; - SmartReplyView.SmartActions smartActions = null; - if (appGeneratedSmartRepliesExist) { - smartReplies = new SmartReplyView.SmartReplies( - Arrays.asList(remoteInputActionPair.first.getChoices()), - remoteInputActionPair.first, - remoteInputActionPair.second.actionIntent, - false /* fromAssistant */); - } - if (appGeneratedSmartActionsExist) { - smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions, - false /* fromAssistant */); - } - // Apps didn't provide any smart replies / actions, use those from NAS (if any). - if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) { - boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.getSmartReplies()) - && freeformRemoteInputActionPair != null - && freeformRemoteInputActionPair.second.getAllowGeneratedReplies() - && freeformRemoteInputActionPair.second.actionIntent != null; - if (useGeneratedReplies) { - smartReplies = new SmartReplyView.SmartReplies( - entry.getSmartReplies(), - freeformRemoteInputActionPair.first, - freeformRemoteInputActionPair.second.actionIntent, - true /* fromAssistant */); - } - boolean useSmartActions = !ArrayUtils.isEmpty(entry.getSmartActions()) - && notification.getAllowSystemGeneratedContextualActions(); - if (useSmartActions) { - List<Notification.Action> systemGeneratedActions = - entry.getSmartActions(); - // Filter actions if we're in kiosk-mode - we don't care about screen pinning mode, - // since notifications aren't shown there anyway. - ActivityManagerWrapper activityManagerWrapper = - Dependency.get(ActivityManagerWrapper.class); - if (activityManagerWrapper.isLockTaskKioskModeActive()) { - systemGeneratedActions = filterWhiteListedLockTaskApps(systemGeneratedActions); - } - smartActions = new SmartReplyView.SmartActions( - systemGeneratedActions, true /* fromAssistant */); - } - } - return new SmartRepliesAndActions(smartReplies, smartActions); - } - - /** - * Filter actions so that only actions pointing to whitelisted apps are allowed. - * This filtering is only meaningful when in lock-task mode. - */ - private static List<Notification.Action> filterWhiteListedLockTaskApps( - List<Notification.Action> actions) { - PackageManagerWrapper packageManagerWrapper = Dependency.get(PackageManagerWrapper.class); - DevicePolicyManagerWrapper devicePolicyManagerWrapper = - Dependency.get(DevicePolicyManagerWrapper.class); - List<Notification.Action> filteredActions = new ArrayList<>(); - for (Notification.Action action : actions) { - if (action.actionIntent == null) continue; - Intent intent = action.actionIntent.getIntent(); - // Only allow actions that are explicit (implicit intents are not handled in lock-task - // mode), and link to whitelisted apps. - ResolveInfo resolveInfo = packageManagerWrapper.resolveActivity(intent, 0 /* flags */); - if (resolveInfo != null && devicePolicyManagerWrapper.isLockTaskPermitted( - resolveInfo.activityInfo.packageName)) { - filteredActions.add(action); - } - } - return filteredActions; - } - - /** - * Returns whether the {@link Notification} represented by entry has a free-form remote input. - * Such an input can be used e.g. to implement smart reply buttons - by passing the replies - * through the remote input. - */ - public static boolean hasFreeformRemoteInput(NotificationEntry entry) { - Notification notification = entry.getSbn().getNotification(); - return null != notification.findRemoteInputActionPair(true /* freeform */); - } - - /** * A storage for smart replies and smart action. */ public static class SmartRepliesAndActions { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java index f52a6e0191a1..f45178cd2230 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcher.java @@ -342,7 +342,7 @@ public class KeyguardUserSwitcher { } v.setActivated(true); } - switchTo(user); + onUserListItemClicked(user); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java index 0fdc80b3d97a..8e8a33fd0d9b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/LocationControllerImpl.java @@ -20,7 +20,6 @@ import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; import static com.android.settingslib.Utils.updateLocationEnabled; -import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -43,6 +42,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.settings.UserTracker; import com.android.systemui.util.Utils; import java.util.ArrayList; @@ -60,6 +60,7 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio private final Context mContext; private final AppOpsController mAppOpsController; private final BootCompleteCache mBootCompleteCache; + private final UserTracker mUserTracker; private final H mHandler; @@ -68,11 +69,13 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio @Inject public LocationControllerImpl(Context context, AppOpsController appOpsController, @Main Looper mainLooper, @Background Handler backgroundHandler, - BroadcastDispatcher broadcastDispatcher, BootCompleteCache bootCompleteCache) { + BroadcastDispatcher broadcastDispatcher, BootCompleteCache bootCompleteCache, + UserTracker userTracker) { mContext = context; mAppOpsController = appOpsController; mBootCompleteCache = bootCompleteCache; mHandler = new H(mainLooper); + mUserTracker = userTracker; // Register to listen for changes in location settings. IntentFilter filter = new IntentFilter(); @@ -113,7 +116,7 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio public boolean setLocationEnabled(boolean enabled) { // QuickSettings always runs as the owner, so specifically set the settings // for the current foreground user. - int currentUserId = ActivityManager.getCurrentUser(); + int currentUserId = mUserTracker.getUserId(); if (isUserLocationRestricted(currentUserId)) { return false; } @@ -134,7 +137,7 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); return mBootCompleteCache.isBootComplete() && locationManager.isLocationEnabledForUser( - UserHandle.of(ActivityManager.getCurrentUser())); + mUserTracker.getUserHandle()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index 0d259fc481db..c15560ae9f38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -151,26 +151,22 @@ public class MobileSignalController extends SignalController< updateDataSim(); int phoneId = mSubscriptionInfo.getSimSlotIndex(); - mFeatureConnector = new FeatureConnector(mContext, phoneId, - new FeatureConnector.Listener<ImsManager> () { - @Override - public ImsManager getFeatureManager() { - return ImsManager.getInstance(mContext, phoneId); - } - - @Override - public void connectionReady(ImsManager manager) throws ImsException { - Log.d(mTag, "ImsManager: connection ready."); - mImsManager = manager; - setListeners(); - } + mFeatureConnector = ImsManager.getConnector( + mContext, phoneId, "?", + new FeatureConnector.Listener<ImsManager> () { + @Override + public void connectionReady(ImsManager manager) throws ImsException { + Log.d(mTag, "ImsManager: connection ready."); + mImsManager = manager; + setListeners(); + } - @Override - public void connectionUnavailable() { - Log.d(mTag, "ImsManager: connection unavailable."); - removeListeners(); - } - }, "?"); + @Override + public void connectionUnavailable(int reason) { + Log.d(mTag, "ImsManager: connection unavailable."); + removeListeners(); + } + }, mContext.getMainExecutor()); mObserver = new ContentObserver(new Handler(receiverLooper)) { @@ -456,8 +452,8 @@ public class MobileSignalController extends SignalController< } try { - mImsManager.addCapabilitiesCallback(mCapabilityCallback); - mImsManager.addRegistrationCallback(mImsRegistrationCallback); + mImsManager.addCapabilitiesCallback(mCapabilityCallback, mContext.getMainExecutor()); + mImsManager.addRegistrationCallback(mImsRegistrationCallback, mContext.getMainExecutor()); Log.d(mTag, "addCapabilitiesCallback " + mCapabilityCallback + " into " + mImsManager); Log.d(mTag, "addRegistrationCallback " + mImsRegistrationCallback + " into " + mImsManager); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartRepliesAndActionsInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartRepliesAndActionsInflater.kt new file mode 100644 index 000000000000..6a3a69c0419e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartRepliesAndActionsInflater.kt @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2020 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.policy + +import android.app.Notification +import android.app.PendingIntent +import android.app.RemoteInput +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.SystemClock +import android.util.Log +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction +import android.widget.Button +import com.android.systemui.R +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.shared.system.ActivityManagerWrapper +import com.android.systemui.shared.system.DevicePolicyManagerWrapper +import com.android.systemui.shared.system.PackageManagerWrapper +import com.android.systemui.statusbar.NotificationRemoteInputManager +import com.android.systemui.statusbar.NotificationUiAdjustment +import com.android.systemui.statusbar.SmartReplyController +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.phone.KeyguardDismissUtil +import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions +import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions +import com.android.systemui.statusbar.policy.SmartReplyView.SmartButtonType +import com.android.systemui.statusbar.policy.SmartReplyView.SmartReplies +import javax.inject.Inject + +/** Returns whether we should show the smart reply view and its smart suggestions. */ +fun shouldShowSmartReplyView( + entry: NotificationEntry, + smartRepliesAndActions: SmartRepliesAndActions +): Boolean { + if (smartRepliesAndActions.smartReplies == null + && smartRepliesAndActions.smartActions == null) { + // There are no smart replies and no smart actions. + return false + } + // If we are showing the spinner we don't want to add the buttons. + val showingSpinner = entry.sbn.notification.extras + .getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false) + if (showingSpinner) { + return false + } + // If we are keeping the notification around while sending we don't want to add the buttons. + return !entry.sbn.notification.extras + .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false) +} + +/** Determines if two [SmartRepliesAndActions] are visually similar. */ +fun areSuggestionsSimilar( + left: SmartRepliesAndActions?, + right: SmartRepliesAndActions? +): Boolean = when { + left === right -> true + left == null || right == null -> false + left.getSmartReplies() != right.getSmartReplies() -> false + else -> !NotificationUiAdjustment.areDifferent(left.getSmartActions(), right.getSmartActions()) +} + +interface SmartRepliesAndActionsInflater { + fun inflateSmartReplies( + sysuiContext: Context, + notifPackageContext: Context, + entry: NotificationEntry, + existingRepliesAndAction: SmartRepliesAndActions? + ): InflatedSmartReplies +} + +/*internal*/ class SmartRepliesAndActionsInflaterImpl @Inject constructor( + private val constants: SmartReplyConstants, + private val activityManagerWrapper: ActivityManagerWrapper, + private val packageManagerWrapper: PackageManagerWrapper, + private val devicePolicyManagerWrapper: DevicePolicyManagerWrapper, + private val smartRepliesInflater: SmartReplyInflater, + private val smartActionsInflater: SmartActionInflater +) : SmartRepliesAndActionsInflater { + + override fun inflateSmartReplies( + sysuiContext: Context, + notifPackageContext: Context, + entry: NotificationEntry, + existingRepliesAndAction: SmartRepliesAndActions? + ): InflatedSmartReplies { + val newRepliesAndActions = chooseSmartRepliesAndActions(entry) + if (!shouldShowSmartReplyView(entry, newRepliesAndActions)) { + return InflatedSmartReplies( + null /* smartReplyView */, + null /* smartSuggestionButtons */, + newRepliesAndActions) + } + + // Only block clicks if the smart buttons are different from the previous set - to avoid + // scenarios where a user incorrectly cannot click smart buttons because the + // notification is updated. + val delayOnClickListener = + !areSuggestionsSimilar(existingRepliesAndAction, newRepliesAndActions) + + val smartReplyView = SmartReplyView.inflate(sysuiContext, constants) + + val smartReplies = newRepliesAndActions.smartReplies + smartReplyView.setSmartRepliesGeneratedByAssistant(smartReplies?.fromAssistant ?: false) + val smartReplyButtons = smartReplies?.let { + smartReplies.choices.asSequence().mapIndexed { index, choice -> + smartRepliesInflater.inflateReplyButton( + smartReplyView, + entry, + smartReplies, + index, + choice, + delayOnClickListener) + } + } ?: emptySequence() + + val smartActionButtons = newRepliesAndActions.smartActions?.let { smartActions -> + val themedPackageContext = + ContextThemeWrapper(notifPackageContext, sysuiContext.theme) + smartActions.actions.asSequence() + .filter { it.actionIntent != null } + .mapIndexed { index, action -> + smartActionsInflater.inflateActionButton( + smartReplyView, + entry, + smartActions, + index, + action, + delayOnClickListener, + themedPackageContext) + } + } ?: emptySequence() + + return InflatedSmartReplies( + smartReplyView, + (smartReplyButtons + smartActionButtons).toList(), + newRepliesAndActions) + } + + /** + * Chose what smart replies and smart actions to display. App generated suggestions take + * precedence. So if the app provides any smart replies, we don't show any + * replies or actions generated by the NotificationAssistantService (NAS), and if the app + * provides any smart actions we also don't show any NAS-generated replies or actions. + */ + fun chooseSmartRepliesAndActions(entry: NotificationEntry): SmartRepliesAndActions { + val notification = entry.sbn.notification + val remoteInputActionPair = notification.findRemoteInputActionPair(false /* freeform */) + val freeformRemoteInputActionPair = + notification.findRemoteInputActionPair(true /* freeform */) + if (!constants.isEnabled) { + if (DEBUG) { + Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for " + + entry.sbn.key) + } + return SmartRepliesAndActions(null, null) + } + // Only use smart replies from the app if they target P or above. We have this check because + // the smart reply API has been used for other things (Wearables) in the past. The API to + // add smart actions is new in Q so it doesn't require a target-sdk check. + val enableAppGeneratedSmartReplies = (!constants.requiresTargetingP() + || entry.targetSdk >= Build.VERSION_CODES.P) + val appGeneratedSmartActions = notification.contextualActions + + var smartReplies: SmartReplies? = when { + enableAppGeneratedSmartReplies -> remoteInputActionPair?.let { pair -> + pair.second.actionIntent?.let { actionIntent -> + if (pair.first.choices?.isNotEmpty() == true) + SmartReplies( + pair.first.choices.asList(), + pair.first, + actionIntent, + false /* fromAssistant */) + else null + } + } + else -> null + } + var smartActions: SmartActions? = when { + appGeneratedSmartActions.isNotEmpty() -> + SmartActions(appGeneratedSmartActions, false /* fromAssistant */) + else -> null + } + // Apps didn't provide any smart replies / actions, use those from NAS (if any). + if (smartReplies == null && smartActions == null) { + val entryReplies = entry.smartReplies + val entryActions = entry.smartActions + if (entryReplies.isNotEmpty() + && freeformRemoteInputActionPair != null + && freeformRemoteInputActionPair.second.allowGeneratedReplies + && freeformRemoteInputActionPair.second.actionIntent != null) { + smartReplies = SmartReplies( + entryReplies, + freeformRemoteInputActionPair.first, + freeformRemoteInputActionPair.second.actionIntent, + true /* fromAssistant */) + } + if (entryActions.isNotEmpty() + && notification.allowSystemGeneratedContextualActions) { + val systemGeneratedActions: List<Notification.Action> = when { + activityManagerWrapper.isLockTaskKioskModeActive -> + // Filter actions if we're in kiosk-mode - we don't care about screen + // pinning mode, since notifications aren't shown there anyway. + filterAllowlistedLockTaskApps(entryActions) + else -> entryActions + } + smartActions = SmartActions(systemGeneratedActions, true /* fromAssistant */) + } + } + return SmartRepliesAndActions(smartReplies, smartActions) + } + + /** + * Filter actions so that only actions pointing to allowlisted apps are permitted. + * This filtering is only meaningful when in lock-task mode. + */ + private fun filterAllowlistedLockTaskApps( + actions: List<Notification.Action> + ): List<Notification.Action> = actions.filter { action -> + // Only allow actions that are explicit (implicit intents are not handled in lock-task + // mode), and link to allowlisted apps. + action.actionIntent?.intent?.let { intent -> + packageManagerWrapper.resolveActivity(intent, 0 /* flags */) + }?.let { resolveInfo -> + devicePolicyManagerWrapper.isLockTaskPermitted(resolveInfo.activityInfo.packageName) + } ?: false + } +} + +interface SmartActionInflater { + fun inflateActionButton( + parent: ViewGroup, + entry: NotificationEntry, + smartActions: SmartActions, + actionIndex: Int, + action: Notification.Action, + delayOnClickListener: Boolean, + packageContext: Context + ): Button +} + +/* internal */ class SmartActionInflaterImpl @Inject constructor( + private val constants: SmartReplyConstants, + private val activityStarter: ActivityStarter, + private val smartReplyController: SmartReplyController, + private val headsUpManager: HeadsUpManager +) : SmartActionInflater { + + override fun inflateActionButton( + parent: ViewGroup, + entry: NotificationEntry, + smartActions: SmartActions, + actionIndex: Int, + action: Notification.Action, + delayOnClickListener: Boolean, + packageContext: Context + ): Button = + (LayoutInflater.from(parent.context) + .inflate(R.layout.smart_action_button, parent, false) as Button + ).apply { + text = action.title + + // We received the Icon from the application - so use the Context of the application to + // reference icon resources. + val iconDrawable = action.getIcon().loadDrawable(packageContext) + .apply { + val newIconSize: Int = context.resources.getDimensionPixelSize( + R.dimen.smart_action_button_icon_size) + setBounds(0, 0, newIconSize, newIconSize) + } + // Add the action icon to the Smart Action button. + setCompoundDrawables(iconDrawable, null, null, null) + + val onClickListener = View.OnClickListener { + onSmartActionClick(entry, smartActions, actionIndex, action) + } + setOnClickListener( + if (delayOnClickListener) + DelayedOnClickListener(onClickListener, constants.onClickInitDelay) + else onClickListener) + + // Mark this as an Action button + (layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.ACTION + } + + private fun onSmartActionClick( + entry: NotificationEntry, + smartActions: SmartActions, + actionIndex: Int, + action: Notification.Action + ) = + activityStarter.startPendingIntentDismissingKeyguard(action.actionIntent, entry.row) { + smartReplyController + .smartActionClicked(entry, actionIndex, action, smartActions.fromAssistant) + headsUpManager.removeNotification(entry.key, true /* releaseImmediately */) + } +} + +interface SmartReplyInflater { + fun inflateReplyButton( + parent: SmartReplyView, + entry: NotificationEntry, + smartReplies: SmartReplies, + replyIndex: Int, + choice: CharSequence, + delayOnClickListener: Boolean + ): Button +} + +class SmartReplyInflaterImpl @Inject constructor( + private val constants: SmartReplyConstants, + private val keyguardDismissUtil: KeyguardDismissUtil, + private val remoteInputManager: NotificationRemoteInputManager, + private val smartReplyController: SmartReplyController, + private val context: Context +) : SmartReplyInflater { + + override fun inflateReplyButton( + parent: SmartReplyView, + entry: NotificationEntry, + smartReplies: SmartReplies, + replyIndex: Int, + choice: CharSequence, + delayOnClickListener: Boolean + ): Button = + (LayoutInflater.from(parent.context) + .inflate(R.layout.smart_reply_button, parent, false) as Button + ).apply { + text = choice + val onClickListener = View.OnClickListener { + onSmartReplyClick( + entry, + smartReplies, + replyIndex, + parent, + this, + choice) + } + setOnClickListener( + if (delayOnClickListener) + DelayedOnClickListener(onClickListener, constants.onClickInitDelay) + else onClickListener) + accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + val label = parent.resources + .getString(R.string.accessibility_send_smart_reply) + val action = AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label) + info.addAction(action) + } + } + // TODO: probably shouldn't do this here, bad API + // Mark this as a Reply button + (layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.REPLY + } + + private fun onSmartReplyClick( + entry: NotificationEntry, + smartReplies: SmartReplies, + replyIndex: Int, + smartReplyView: SmartReplyView, + button: Button, + choice: CharSequence + ) = keyguardDismissUtil.executeWhenUnlocked(!entry.isRowPinned) { + val canEditBeforeSend = constants.getEffectiveEditChoicesBeforeSending( + smartReplies.remoteInput.editChoicesBeforeSending) + if (canEditBeforeSend) { + remoteInputManager.activateRemoteInput( + button, + arrayOf(smartReplies.remoteInput), + smartReplies.remoteInput, + smartReplies.pendingIntent, + NotificationEntry.EditedSuggestionInfo(choice, replyIndex)) + } else { + smartReplyController.smartReplySent( + entry, + replyIndex, + button.text, + NotificationLogger.getNotificationLocation(entry).toMetricsEventEnum(), + false /* modifiedBeforeSending */) + entry.setHasSentReply() + try { + val intent = createRemoteInputIntent(smartReplies, choice) + smartReplies.pendingIntent.send(context, 0, intent) + } catch (e: PendingIntent.CanceledException) { + Log.w(TAG, "Unable to send smart reply", e) + } + smartReplyView.hideSmartSuggestions() + } + false // do not defer + } + + private fun createRemoteInputIntent(smartReplies: SmartReplies, choice: CharSequence): Intent { + val results = Bundle() + results.putString(smartReplies.remoteInput.resultKey, choice.toString()) + val intent = Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + RemoteInput.addResultsToIntent(arrayOf(smartReplies.remoteInput), intent, results) + RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE) + return intent + } +} + +/** + * An OnClickListener wrapper that blocks the underlying OnClickListener for a given amount of + * time. + */ +private class DelayedOnClickListener( + private val mActualListener: View.OnClickListener, + private val mInitDelayMs: Long +) : View.OnClickListener { + + private val mInitTimeMs = SystemClock.elapsedRealtime() + + override fun onClick(v: View) { + if (hasFinishedInitialization()) { + mActualListener.onClick(v) + } else { + Log.i(TAG, "Accidental Smart Suggestion click registered, delay: $mInitDelayMs") + } + } + + private fun hasFinishedInitialization(): Boolean = + SystemClock.elapsedRealtime() >= mInitTimeMs + mInitDelayMs +} + +private const val TAG = "SmartReplyViewInflater" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +// convenience function that swaps parameter order so that lambda can be placed at the end +private fun KeyguardDismissUtil.executeWhenUnlocked( + requiresShadeOpen: Boolean, + onDismissAction: () -> Boolean +) = executeWhenUnlocked(onDismissAction, requiresShadeOpen) + +// convenience function that swaps parameter order so that lambda can be placed at the end +private fun ActivityStarter.startPendingIntentDismissingKeyguard( + intent: PendingIntent, + associatedView: View?, + runnable: () -> Unit +) = startPendingIntentDismissingKeyguard(intent, runnable::invoke, associatedView)
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java index 949ac4df88ba..e7f84a55eb5f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java @@ -6,7 +6,6 @@ import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteInput; import android.content.Context; -import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -15,34 +14,20 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.RippleDrawable; -import android.os.Bundle; -import android.os.SystemClock; import android.text.Layout; import android.text.TextPaint; import android.text.method.TransformationMethod; import android.util.AttributeSet; import android.util.Log; -import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.Button; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ContrastColorUtil; -import com.android.systemui.Dependency; import com.android.systemui.R; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.ActivityStarter.OnDismissAction; -import com.android.systemui.statusbar.NotificationRemoteInputManager; -import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.notification.NotificationUtils; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; -import com.android.systemui.statusbar.notification.logging.NotificationLogger; -import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import java.text.BreakIterator; import java.util.ArrayList; @@ -64,10 +49,6 @@ public class SmartReplyView extends ViewGroup { private static final int SQUEEZE_FAILED = -1; - private final SmartReplyConstants mConstants; - private final KeyguardDismissUtil mKeyguardDismissUtil; - private final NotificationRemoteInputManager mRemoteInputManager; - /** * The upper bound for the height of this view in pixels. Notifications are automatically * recreated on density or font size changes so caching this should be fine. @@ -98,30 +79,25 @@ public class SmartReplyView extends ViewGroup { */ private boolean mSmartRepliesGeneratedByAssistant = false; - @ColorInt - private int mCurrentBackgroundColor; - @ColorInt - private final int mDefaultBackgroundColor; - @ColorInt - private final int mDefaultStrokeColor; - @ColorInt - private final int mDefaultTextColor; - @ColorInt - private final int mDefaultTextColorDarkBg; - @ColorInt - private final int mRippleColorDarkBg; - @ColorInt - private final int mRippleColor; + @ColorInt private int mCurrentBackgroundColor; + @ColorInt private final int mDefaultBackgroundColor; + @ColorInt private final int mDefaultStrokeColor; + @ColorInt private final int mDefaultTextColor; + @ColorInt private final int mDefaultTextColorDarkBg; + @ColorInt private final int mRippleColorDarkBg; + @ColorInt private final int mRippleColor; private final int mStrokeWidth; private final double mMinStrokeContrast; - private ActivityStarter mActivityStarter; + @ColorInt private int mCurrentStrokeColor; + @ColorInt private int mCurrentTextColor; + @ColorInt private int mCurrentRippleColor; + private int mMaxSqueezeRemeasureAttempts; + private int mMaxNumActions; + private int mMinNumSystemGeneratedReplies; public SmartReplyView(Context context, AttributeSet attrs) { super(context, attrs); - mConstants = Dependency.get(SmartReplyConstants.class); - mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class); - mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class); mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext, R.dimen.smart_reply_button_max_height); @@ -172,6 +148,18 @@ public class SmartReplyView extends ViewGroup { } /** + * Inflate an instance of this class. + */ + public static SmartReplyView inflate(Context context, SmartReplyConstants constants) { + SmartReplyView view = (SmartReplyView) LayoutInflater.from(context).inflate( + R.layout.smart_reply_view, null /* root */); + view.setMaxNumActions(constants.getMaxNumActions()); + view.setMaxSqueezeRemeasureAttempts(constants.getMaxSqueezeRemeasureAttempts()); + view.setMinNumSystemGeneratedReplies(constants.getMinNumSystemGeneratedReplies()); + return view; + } + + /** * Returns an upper bound for the height of this view in pixels. This method is intended to be * invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons. */ @@ -197,174 +185,25 @@ public class SmartReplyView extends ViewGroup { mCurrentBackgroundColor = mDefaultBackgroundColor; } - /** - * Add buttons to the {@link SmartReplyView} - these buttons must have been preinflated using - * one of the methods in this class. - */ + /** Add buttons to the {@link SmartReplyView} */ public void addPreInflatedButtons(List<Button> smartSuggestionButtons) { for (Button button : smartSuggestionButtons) { addView(button); + setButtonColors(button); } reallocateCandidateButtonQueueForSqueezing(); } - /** - * Add smart replies to this view, using the provided {@link RemoteInput} and - * {@link PendingIntent} to respond when the user taps a smart reply. Only the replies that fit - * into the notification are shown. - */ - public List<Button> inflateRepliesFromRemoteInput( - @NonNull SmartReplies smartReplies, - SmartReplyController smartReplyController, NotificationEntry entry, - boolean delayOnClickListener) { - List<Button> buttons = new ArrayList<>(); - - if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) { - if (smartReplies.choices != null) { - for (int i = 0; i < smartReplies.choices.size(); ++i) { - buttons.add(inflateReplyButton( - this, getContext(), i, smartReplies, smartReplyController, entry, - delayOnClickListener)); - } - this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant; - } - } - return buttons; - } - - /** - * Add smart actions to be shown next to smart replies. Only the actions that fit into the - * notification are shown. - */ - public List<Button> inflateSmartActions(Context packageContext, - @NonNull SmartActions smartActions, SmartReplyController smartReplyController, - NotificationEntry entry, HeadsUpManager headsUpManager, boolean delayOnClickListener) { - Context themedPackageContext = new ContextThemeWrapper(packageContext, mContext.getTheme()); - List<Button> buttons = new ArrayList<>(); - int numSmartActions = smartActions.actions.size(); - for (int n = 0; n < numSmartActions; n++) { - Notification.Action action = smartActions.actions.get(n); - if (action.actionIntent != null) { - buttons.add(inflateActionButton( - this, getContext(), themedPackageContext, n, smartActions, - smartReplyController, - entry, headsUpManager, delayOnClickListener)); - } - } - return buttons; - } - - /** - * Inflate an instance of this class. - */ - public static SmartReplyView inflate(Context context) { - return (SmartReplyView) LayoutInflater.from(context).inflate( - R.layout.smart_reply_view, null /* root */); + public void setMaxNumActions(int maxNumActions) { + mMaxNumActions = maxNumActions; } - @VisibleForTesting - static Button inflateReplyButton(SmartReplyView smartReplyView, Context context, - int replyIndex, SmartReplies smartReplies, SmartReplyController smartReplyController, - NotificationEntry entry, boolean useDelayedOnClickListener) { - Button b = (Button) LayoutInflater.from(context).inflate( - R.layout.smart_reply_button, smartReplyView, false); - CharSequence choice = smartReplies.choices.get(replyIndex); - b.setText(choice); - - OnDismissAction action = () -> { - if (smartReplyView.mConstants.getEffectiveEditChoicesBeforeSending( - smartReplies.remoteInput.getEditChoicesBeforeSending())) { - EditedSuggestionInfo editedSuggestionInfo = - new EditedSuggestionInfo(choice, replyIndex); - smartReplyView.mRemoteInputManager.activateRemoteInput(b, - new RemoteInput[] { smartReplies.remoteInput }, smartReplies.remoteInput, - smartReplies.pendingIntent, editedSuggestionInfo); - return false; - } - - smartReplyController.smartReplySent(entry, replyIndex, b.getText(), - NotificationLogger.getNotificationLocation(entry).toMetricsEventEnum(), - false /* modifiedBeforeSending */); - Bundle results = new Bundle(); - results.putString(smartReplies.remoteInput.getResultKey(), choice.toString()); - Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - RemoteInput.addResultsToIntent(new RemoteInput[] { smartReplies.remoteInput }, intent, - results); - RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE); - entry.setHasSentReply(); - try { - smartReplies.pendingIntent.send(context, 0, intent); - } catch (PendingIntent.CanceledException e) { - Log.w(TAG, "Unable to send smart reply", e); - } - // Note that as inflateReplyButton is called mSmartReplyContainer is null, but when the - // reply Button is added to the SmartReplyView mSmartReplyContainer will be set. So, it - // will not be possible for a user to trigger this on-click-listener without - // mSmartReplyContainer being set. - smartReplyView.mSmartReplyContainer.setVisibility(View.GONE); - return false; // do not defer - }; - - OnClickListener onClickListener = view -> - smartReplyView.mKeyguardDismissUtil.executeWhenUnlocked(action, !entry.isRowPinned()); - if (useDelayedOnClickListener) { - onClickListener = new DelayedOnClickListener(onClickListener, - smartReplyView.mConstants.getOnClickInitDelay()); - } - b.setOnClickListener(onClickListener); - - b.setAccessibilityDelegate(new AccessibilityDelegate() { - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - String label = smartReplyView.getResources().getString( - R.string.accessibility_send_smart_reply); - info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label)); - } - }); - - SmartReplyView.setButtonColors(b, smartReplyView.mCurrentBackgroundColor, - smartReplyView.mDefaultStrokeColor, smartReplyView.mDefaultTextColor, - smartReplyView.mRippleColor, smartReplyView.mStrokeWidth); - return b; + public void setMinNumSystemGeneratedReplies(int minNumSystemGeneratedReplies) { + mMinNumSystemGeneratedReplies = minNumSystemGeneratedReplies; } - @VisibleForTesting - static Button inflateActionButton(SmartReplyView smartReplyView, Context context, - Context packageContext, int actionIndex, SmartActions smartActions, - SmartReplyController smartReplyController, NotificationEntry entry, - HeadsUpManager headsUpManager, boolean useDelayedOnClickListener) { - Notification.Action action = smartActions.actions.get(actionIndex); - Button button = (Button) LayoutInflater.from(context).inflate( - R.layout.smart_action_button, smartReplyView, false); - button.setText(action.title); - - // We received the Icon from the application - so use the Context of the application to - // reference icon resources. - Drawable iconDrawable = action.getIcon().loadDrawable(packageContext); - // Add the action icon to the Smart Action button. - int newIconSize = context.getResources().getDimensionPixelSize( - R.dimen.smart_action_button_icon_size); - iconDrawable.setBounds(0, 0, newIconSize, newIconSize); - button.setCompoundDrawables(iconDrawable, null, null, null); - - OnClickListener onClickListener = view -> - smartReplyView.getActivityStarter().startPendingIntentDismissingKeyguard( - action.actionIntent, - () -> { - smartReplyController.smartActionClicked( - entry, actionIndex, action, smartActions.fromAssistant); - headsUpManager.removeNotification(entry.getKey(), true); - }, entry.getRow()); - if (useDelayedOnClickListener) { - onClickListener = new DelayedOnClickListener(onClickListener, - smartReplyView.mConstants.getOnClickInitDelay()); - } - button.setOnClickListener(onClickListener); - - // Mark this as an Action button - final LayoutParams lp = (LayoutParams) button.getLayoutParams(); - lp.buttonType = SmartButtonType.ACTION; - return button; + public void setMaxSqueezeRemeasureAttempts(int maxSqueezeRemeasureAttempts) { + mMaxSqueezeRemeasureAttempts = maxSqueezeRemeasureAttempts; } @Override @@ -416,13 +255,13 @@ public class SmartReplyView extends ViewGroup { // reply button is added. SmartSuggestionMeasures actionsMeasures = null; - final int maxNumActions = mConstants.getMaxNumActions(); + final int maxNumActions = mMaxNumActions; int numShownActions = 0; for (View child : smartSuggestions) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (maxNumActions != -1 // -1 means 'no limit' - && lp.buttonType == SmartButtonType.ACTION + && lp.mButtonType == SmartButtonType.ACTION && numShownActions >= maxNumActions) { // We've reached the maximum number of actions, don't add another one! continue; @@ -446,7 +285,7 @@ public class SmartReplyView extends ViewGroup { // Remember the current measurements in case the current button doesn't fit in. SmartSuggestionMeasures originalMeasures = accumulatedMeasures.clone(); - if (actionsMeasures == null && lp.buttonType == SmartButtonType.REPLY) { + if (actionsMeasures == null && lp.mButtonType == SmartButtonType.REPLY) { // We've added all actions (we go through actions first), now add their // measurements. actionsMeasures = accumulatedMeasures.clone(); @@ -510,7 +349,7 @@ public class SmartReplyView extends ViewGroup { lp.show = true; displayedChildCount++; - if (lp.buttonType == SmartButtonType.ACTION) { + if (lp.mButtonType == SmartButtonType.ACTION) { numShownActions++; } } @@ -551,6 +390,19 @@ public class SmartReplyView extends ViewGroup { resolveSize(buttonHeight, heightMeasureSpec)); } + // TODO: this should be replaced, and instead, setMinSystemGenerated... should be invoked + // with MAX_VALUE if mSmartRepliesGeneratedByAssistant would be false (essentially, this is a + // ViewModel decision, as opposed to a View decision) + void setSmartRepliesGeneratedByAssistant(boolean fromAssistant) { + mSmartRepliesGeneratedByAssistant = fromAssistant; + } + + void hideSmartSuggestions() { + if (mSmartReplyContainer != null) { + mSmartReplyContainer.setVisibility(View.GONE); + } + } + /** * Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending * on which suggestions are added. @@ -577,6 +429,7 @@ public class SmartReplyView extends ViewGroup { * Returns whether our notification contains at least N smart replies (or 0) where N is * determined by {@link SmartReplyConstants}. */ + // TODO: we probably sholdn't make this deliberation in the View private boolean gotEnoughSmartReplies(List<View> smartReplies) { int numShownReplies = 0; for (View smartReplyButton : smartReplies) { @@ -585,8 +438,7 @@ public class SmartReplyView extends ViewGroup { numShownReplies++; } } - if (numShownReplies == 0 - || numShownReplies >= mConstants.getMinNumSystemGeneratedReplies()) { + if (numShownReplies == 0 || numShownReplies >= mMinNumSystemGeneratedReplies) { // We have enough replies, yay! return true; } @@ -602,7 +454,7 @@ public class SmartReplyView extends ViewGroup { if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) { continue; } - if (lp.buttonType == buttonType) { + if (lp.mButtonType == buttonType) { actions.add(child); } } @@ -656,7 +508,7 @@ public class SmartReplyView extends ViewGroup { // See if there's a better line-break point (leading to a more narrow button) in // either left or right direction. final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth; - final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts(); + final int maxSqueezeRemeasureAttempts = mMaxSqueezeRemeasureAttempts; for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) { final int newPosition = moveLeft ? mBreakIterator.previous() : mBreakIterator.next(); @@ -833,41 +685,38 @@ public class SmartReplyView extends ViewGroup { final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor); - int textColor = ContrastColorUtil.ensureTextContrast( + mCurrentTextColor = ContrastColorUtil.ensureTextContrast( dark ? mDefaultTextColorDarkBg : mDefaultTextColor, backgroundColor | 0xff000000, dark); - int strokeColor = ContrastColorUtil.ensureContrast( + mCurrentStrokeColor = ContrastColorUtil.ensureContrast( mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast); - int rippleColor = dark ? mRippleColorDarkBg : mRippleColor; + mCurrentRippleColor = dark ? mRippleColorDarkBg : mRippleColor; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { - final Button child = (Button) getChildAt(i); - setButtonColors(child, backgroundColor, strokeColor, textColor, rippleColor, - mStrokeWidth); + setButtonColors((Button) getChildAt(i)); } } - private static void setButtonColors(Button button, int backgroundColor, int strokeColor, - int textColor, int rippleColor, int strokeWidth) { + private void setButtonColors(Button button) { Drawable drawable = button.getBackground(); if (drawable instanceof RippleDrawable) { // Mutate in case other notifications are using this drawable. drawable = drawable.mutate(); RippleDrawable ripple = (RippleDrawable) drawable; - ripple.setColor(ColorStateList.valueOf(rippleColor)); + ripple.setColor(ColorStateList.valueOf(mCurrentRippleColor)); Drawable inset = ripple.getDrawable(0); if (inset instanceof InsetDrawable) { Drawable background = ((InsetDrawable) inset).getDrawable(); if (background instanceof GradientDrawable) { GradientDrawable gradientDrawable = (GradientDrawable) background; - gradientDrawable.setColor(backgroundColor); - gradientDrawable.setStroke(strokeWidth, strokeColor); + gradientDrawable.setColor(mCurrentBackgroundColor); + gradientDrawable.setStroke(mStrokeWidth, mCurrentStrokeColor); } } button.setBackground(drawable); } - button.setTextColor(textColor); + button.setTextColor(mCurrentTextColor); } private void setCornerRadius(Button button, float radius) { @@ -887,14 +736,7 @@ public class SmartReplyView extends ViewGroup { } } - private ActivityStarter getActivityStarter() { - if (mActivityStarter == null) { - mActivityStarter = Dependency.get(ActivityStarter.class); - } - return mActivityStarter; - } - - private enum SmartButtonType { + enum SmartButtonType { REPLY, ACTION } @@ -924,7 +766,7 @@ public class SmartReplyView extends ViewGroup { private boolean show = false; private int squeezeStatus = SQUEEZE_STATUS_NONE; - private SmartButtonType buttonType = SmartButtonType.REPLY; + SmartButtonType mButtonType = SmartButtonType.REPLY; private LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); @@ -975,32 +817,4 @@ public class SmartReplyView extends ViewGroup { this.fromAssistant = fromAssistant; } } - - /** - * An OnClickListener wrapper that blocks the underlying OnClickListener for a given amount of - * time. - */ - private static class DelayedOnClickListener implements OnClickListener { - private final OnClickListener mActualListener; - private final long mInitDelayMs; - private final long mInitTimeMs; - - DelayedOnClickListener(OnClickListener actualOnClickListener, long initDelayMs) { - mActualListener = actualOnClickListener; - mInitDelayMs = initDelayMs; - mInitTimeMs = SystemClock.elapsedRealtime(); - } - - public void onClick(View v) { - if (hasFinishedInitialization()) { - mActualListener.onClick(v); - } else { - Log.i(TAG, "Accidental Smart Suggestion click registered, delay: " + mInitDelayMs); - } - } - - private boolean hasFinishedInitialization() { - return SystemClock.elapsedRealtime() >= mInitTimeMs + mInitDelayMs; - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 17fcb1dd6f1a..72e8e38735e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -23,6 +23,7 @@ import static com.android.systemui.DejankUtils.whitelistIpcs; import android.app.ActivityManager; import android.app.Dialog; +import android.app.IActivityTaskManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -53,7 +54,6 @@ import android.widget.BaseAdapter; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.util.UserIcons; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.systemui.Dumpable; import com.android.systemui.GuestResumeSessionReceiver; @@ -69,6 +69,7 @@ import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.tiles.UserDetailView; import com.android.systemui.statusbar.phone.SystemUIDialog; +import com.android.systemui.user.CreateUserActivity; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -104,6 +105,7 @@ public class UserSwitcherController implements Dumpable { protected final Handler mHandler; private final ActivityStarter mActivityStarter; private final BroadcastDispatcher mBroadcastDispatcher; + private final IActivityTaskManager mActivityTaskManager; private ArrayList<UserRecord> mUsers = new ArrayList<>(); private Dialog mExitGuestDialog; @@ -121,9 +123,11 @@ public class UserSwitcherController implements Dumpable { @Inject public UserSwitcherController(Context context, KeyguardStateController keyguardStateController, @Main Handler handler, ActivityStarter activityStarter, - BroadcastDispatcher broadcastDispatcher, UiEventLogger uiEventLogger) { + BroadcastDispatcher broadcastDispatcher, UiEventLogger uiEventLogger, + IActivityTaskManager activityTaskManager) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; + mActivityTaskManager = activityTaskManager; mUiEventLogger = uiEventLogger; if (!UserManager.isGuestUserEphemeral()) { mGuestResumeSessionReceiver.register(mBroadcastDispatcher); @@ -363,7 +367,7 @@ public class UserSwitcherController implements Dumpable { } } - public void switchTo(UserRecord record) { + private void onUserListItemClicked(UserRecord record) { int id; if (record.isGuest && record.info == null) { // No guest user. Create one. @@ -408,19 +412,6 @@ public class UserSwitcherController implements Dumpable { switchToUserId(id); } - public void switchTo(int userId) { - final int count = mUsers.size(); - for (int i = 0; i < count; ++i) { - UserRecord record = mUsers.get(i); - if (record.info != null && record.info.id == userId) { - switchTo(record); - return; - } - } - - Log.e(TAG, "Couldn't switch to user, id=" + userId); - } - protected void switchToUserId(int id) { try { pauseRefreshUsers(); @@ -666,8 +657,11 @@ public class UserSwitcherController implements Dumpable { return position; } - public void switchTo(UserRecord record) { - mController.switchTo(record); + /** + * It handles click events on user list items. + */ + public void onUserListItemClicked(UserRecord record) { + mController.onUserListItemClicked(record); } public String getName(Context context, UserRecord item) { @@ -924,18 +918,33 @@ public class UserSwitcherController implements Dumpable { if (ActivityManager.isUserAMonkey()) { return; } - UserInfo user = mUserManager.createUser( - mContext.getString(R.string.user_new_user_name), 0 /* flags */); - if (user == null) { - // Couldn't create user, most likely because there are too many, but we haven't - // been able to reload the list yet. - return; + Intent intent = CreateUserActivity.createIntentForStart(getContext()); + + // There are some differences between ActivityStarter and ActivityTaskManager in + // terms of how they start an activity. ActivityStarter hides the notification bar + // before starting the activity to make sure nothing is in front of the new + // activity. ActivityStarter also tries to unlock the device if it's locked. + // When locked with PIN/pattern/password then it shows the prompt, if there are no + // security steps then it dismisses the keyguard and then starts the activity. + // ActivityTaskManager doesn't hide the notification bar or unlocks the device, but + // it can start an activity on top of the locked screen. + if (!mKeyguardStateController.isUnlocked() + && !mKeyguardStateController.canDismissLockScreen()) { + // Device is locked and can't be unlocked without a PIN/pattern/password so we + // need to use ActivityTaskManager to start the activity on top of the locked + // screen. + try { + mActivityTaskManager.startActivity(null, + mContext.getBasePackageName(), mContext.getAttributionTag(), intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), null, + null, 0, 0, null, null); + } catch (RemoteException e) { + e.printStackTrace(); + Log.e(TAG, "Couldn't start create user activity", e); + } + } else { + mActivityStarter.startActivity(intent, true); } - int id = user.id; - Bitmap icon = UserIcons.convertToBitmap(UserIcons.getDefaultUserIcon( - mContext.getResources(), id, /* light= */ false)); - mUserManager.setUserIcon(id, icon); - switchToUserId(id); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/SmartRepliesInflationModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/SmartRepliesInflationModule.kt new file mode 100644 index 000000000000..803d26ec3286 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/SmartRepliesInflationModule.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 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.policy.dagger + +import com.android.systemui.statusbar.policy.SmartActionInflater +import com.android.systemui.statusbar.policy.SmartActionInflaterImpl +import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflater +import com.android.systemui.statusbar.policy.SmartRepliesAndActionsInflaterImpl +import com.android.systemui.statusbar.policy.SmartReplyInflater +import com.android.systemui.statusbar.policy.SmartReplyInflaterImpl +import dagger.Binds +import dagger.Module + +@Module +interface SmartRepliesInflationModule { + @Binds fun bindSmartActionsInflater(impl: SmartActionInflaterImpl): SmartActionInflater + @Binds fun bindSmartReplyInflater(impl: SmartReplyInflaterImpl): SmartReplyInflater + @Binds fun bindsInflatedSmartRepliesProvider( + impl: SmartRepliesAndActionsInflaterImpl + ): SmartRepliesAndActionsInflater +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java index 7a78c157e5b4..0bd36240a366 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java @@ -58,6 +58,10 @@ public class TvNotificationPanel extends SystemUI implements CommandQueue.Callba if (!mNotificationHandlerPackage.isEmpty()) { startNotificationHandlerActivity( new Intent(NotificationManager.ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL)); + } else { + Log.w(TAG, + "Not toggling notification panel: config_notificationHandlerPackage is " + + "empty"); } } @@ -66,6 +70,10 @@ public class TvNotificationPanel extends SystemUI implements CommandQueue.Callba if (!mNotificationHandlerPackage.isEmpty()) { startNotificationHandlerActivity( new Intent(NotificationManager.ACTION_OPEN_NOTIFICATION_HANDLER_PANEL)); + } else { + Log.w(TAG, + "Not expanding notification panel: config_notificationHandlerPackage is " + + "empty"); } } @@ -77,6 +85,9 @@ public class TvNotificationPanel extends SystemUI implements CommandQueue.Callba NotificationManager.ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL); closeNotificationIntent.setPackage(mNotificationHandlerPackage); mContext.sendBroadcastAsUser(closeNotificationIntent, UserHandle.CURRENT); + } else { + Log.w(TAG, + "Not closing notification panel: config_notificationHandlerPackage is empty"); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java index a29db4d98329..c9d1b71bca77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java @@ -21,7 +21,6 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.IntDef; import android.annotation.UiThread; @@ -36,7 +35,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewTreeObserver; import android.view.WindowManager; -import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.statusbar.tv.TvStatusBar; @@ -83,19 +81,14 @@ public class AudioRecordingDisclosureBar implements private static final int STATE_SHOWN = 2; private static final int STATE_DISAPPEARING = 3; - private static final int ANIMATION_DURATION = 600; + private static final int ANIMATION_DURATION_MS = 200; private final Context mContext; private boolean mIsEnabled; private View mIndicatorView; - private View mIconTextsContainer; - private View mIconContainerBg; - private View mIcon; - private View mBgEnd; - private View mTextsContainers; - private TextView mTextView; - private boolean mIsLtr; + private boolean mViewAndWindowAdded; + private ObjectAnimator mAnimator; @State private int mState = STATE_STOPPED; @@ -190,7 +183,7 @@ public class AudioRecordingDisclosureBar implements } if (active) { - showIfNotShown(); + showIfNeeded(); } else { hideIndicatorIfNeeded(); } @@ -198,152 +191,145 @@ public class AudioRecordingDisclosureBar implements @UiThread private void hideIndicatorIfNeeded() { - // If not STATE_APPEARING, will check whether the indicator should be hidden when the - // indicator comes to the STATE_SHOWN. - // If STATE_DISAPPEARING or STATE_SHOWN - nothing else for us to do here. - if (mState != STATE_SHOWN) return; - - // If is in the STATE_SHOWN and there are no active recorders - hide. - if (!hasActiveRecorders()) { - hide(); + // If STOPPED, NOT_SHOWN or DISAPPEARING - nothing else for us to do here. + if (mState != STATE_SHOWN && mState != STATE_APPEARING) return; + + if (hasActiveRecorders()) { + return; + } + + if (mViewAndWindowAdded) { + mState = STATE_DISAPPEARING; + animateDisappearance(); + } else { + // Appearing animation has not started yet, as we were still waiting for the View to be + // laid out. + mState = STATE_NOT_SHOWN; + removeIndicatorView(); } } @UiThread - private void showIfNotShown() { - if (mState != STATE_NOT_SHOWN) return; + private void showIfNeeded() { + // If STOPPED, SHOWN or APPEARING - nothing else for us to do here. + if (mState != STATE_NOT_SHOWN && mState != STATE_DISAPPEARING) return; + if (DEBUG) Log.d(TAG, "Showing indicator"); - mIsLtr = mContext.getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; + final int prevState = mState; + mState = STATE_APPEARING; + + if (prevState == STATE_DISAPPEARING) { + animateAppearance(); + return; + } // Inflate the indicator view mIndicatorView = LayoutInflater.from(mContext).inflate( - R.layout.tv_audio_recording_indicator, - null); - mIconTextsContainer = mIndicatorView.findViewById(R.id.icon_texts_container); - mIconContainerBg = mIconTextsContainer.findViewById(R.id.icon_container_bg); - mIcon = mIconTextsContainer.findViewById(R.id.icon_mic); - mTextsContainers = mIconTextsContainer.findViewById(R.id.texts_container); - mTextView = mTextsContainers.findViewById(R.id.text); - mBgEnd = mIndicatorView.findViewById(R.id.bg_end); - - mTextsContainers.setVisibility(View.GONE); - mIconContainerBg.setVisibility(View.GONE); - mTextView.setVisibility(View.GONE); - mBgEnd.setVisibility(View.GONE); - mTextsContainers = null; - mIconContainerBg = null; - mTextView = null; - mBgEnd = null; - - // Initially change the visibility to INVISIBLE, wait until and receives the size and - // then animate it moving from "off" the screen correctly - mIndicatorView.setVisibility(View.INVISIBLE); + R.layout.tv_audio_recording_indicator, null); + + // 1. Set alpha to 0. + // 2. Wait until the window is shown and the view is laid out. + // 3. Start a "fade in" (alpha) animation. + mIndicatorView.setAlpha(0f); mIndicatorView .getViewTreeObserver() .addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { - if (mState == STATE_STOPPED) { - return; - } + // State could have changed to NOT_SHOWN (if all the recorders are + // already gone) to STOPPED (if the indicator was disabled) + if (mState != STATE_APPEARING) return; + mViewAndWindowAdded = true; // Remove the observer mIndicatorView.getViewTreeObserver().removeOnGlobalLayoutListener( this); - // Now that the width of the indicator has been assigned, we can - // move it in from off the screen. - final int initialOffset = - (mIsLtr ? 1 : -1) * mIndicatorView.getWidth(); - final AnimatorSet set = new AnimatorSet(); - set.setDuration(ANIMATION_DURATION); - set.playTogether( - ObjectAnimator.ofFloat(mIndicatorView, - View.TRANSLATION_X, initialOffset, 0), - ObjectAnimator.ofFloat(mIndicatorView, View.ALPHA, 0f, - 1f)); - set.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation, - boolean isReverse) { - if (mState == STATE_STOPPED) return; - - // Indicator is INVISIBLE at the moment, change it. - mIndicatorView.setVisibility(View.VISIBLE); - } - - @Override - public void onAnimationEnd(Animator animation) { - onAppeared(); - } - }); - set.start(); + animateAppearance(); } }); + final boolean isLtr = mContext.getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_LTR; final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( WRAP_CONTENT, WRAP_CONTENT, WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); - layoutParams.gravity = Gravity.TOP | (mIsLtr ? Gravity.RIGHT : Gravity.LEFT); + layoutParams.gravity = Gravity.TOP | (isLtr ? Gravity.RIGHT : Gravity.LEFT); layoutParams.setTitle(LAYOUT_PARAMS_TITLE); layoutParams.packageName = mContext.getPackageName(); final WindowManager windowManager = (WindowManager) mContext.getSystemService( Context.WINDOW_SERVICE); windowManager.addView(mIndicatorView, layoutParams); - - mState = STATE_APPEARING; } - @UiThread - private void hide() { - if (DEBUG) Log.d(TAG, "Hide indicator"); - - final int targetOffset = (mIsLtr ? 1 : -1) * (mIndicatorView.getWidth() - - (int) mIconTextsContainer.getTranslationX()); - final AnimatorSet set = new AnimatorSet(); - set.playTogether( - ObjectAnimator.ofFloat(mIndicatorView, View.TRANSLATION_X, targetOffset), - ObjectAnimator.ofFloat(mIcon, View.ALPHA, 0f)); - set.setDuration(ANIMATION_DURATION); - set.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - onHidden(); - } - }); - set.start(); - mState = STATE_DISAPPEARING; + private void animateAppearance() { + animateAlphaTo(1f); + } + + private void animateDisappearance() { + animateAlphaTo(0f); } + private void animateAlphaTo(final float endValue) { + if (mAnimator == null) { + if (DEBUG) Log.d(TAG, "set up animator"); - @UiThread - private void onAppeared() { - if (mState == STATE_STOPPED) return; + mAnimator = new ObjectAnimator(); + mAnimator.setTarget(mIndicatorView); + mAnimator.setProperty(View.ALPHA); + mAnimator.addListener(new AnimatorListenerAdapter() { + boolean mCancelled; - mState = STATE_SHOWN; + @Override + public void onAnimationStart(Animator animation, boolean isReverse) { + if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationStart"); + mCancelled = false; + } - hideIndicatorIfNeeded(); - } + @Override + public void onAnimationCancel(Animator animation) { + if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationCancel"); + mCancelled = true; + } - @UiThread - private void onHidden() { - if (mState == STATE_STOPPED) return; + @Override + public void onAnimationEnd(Animator animation) { + if (DEBUG) Log.d(TAG, "AnimatorListenerAdapter#onAnimationEnd"); + // When ValueAnimator#cancel() is called it always calls onAnimationCancel(...) + // and then onAnimationEnd(...). We, however, only want to proceed here if the + // animation ended "naturally". + if (!mCancelled) { + onAnimationFinished(); + } + } + }); + } else if (mAnimator.isRunning()) { + if (DEBUG) Log.d(TAG, "cancel running animation"); + mAnimator.cancel(); + } - removeIndicatorView(); - mState = STATE_NOT_SHOWN; + final float currentValue = mIndicatorView.getAlpha(); + if (DEBUG) Log.d(TAG, "animate alpha to " + endValue + " from " + currentValue); - if (hasActiveRecorders()) { - // Got new recorders, show again. - showIfNotShown(); + mAnimator.setDuration((int) (Math.abs(currentValue - endValue) * ANIMATION_DURATION_MS)); + mAnimator.setFloatValues(endValue); + mAnimator.start(); + } + + private void onAnimationFinished() { + if (DEBUG) Log.d(TAG, "onAnimationFinished"); + + if (mState == STATE_APPEARING) { + mState = STATE_SHOWN; + } else if (mState == STATE_DISAPPEARING) { + removeIndicatorView(); + mState = STATE_NOT_SHOWN; } } @@ -358,17 +344,16 @@ public class AudioRecordingDisclosureBar implements } private void removeIndicatorView() { + if (DEBUG) Log.d(TAG, "removeIndicatorView"); + final WindowManager windowManager = (WindowManager) mContext.getSystemService( Context.WINDOW_SERVICE); windowManager.removeView(mIndicatorView); mIndicatorView = null; - mIconTextsContainer = null; - mIconContainerBg = null; - mIcon = null; - mTextsContainers = null; - mTextView = null; - mBgEnd = null; + mAnimator = null; + + mViewAndWindowAdded = false; } private static List<String> splitByComma(String string) { diff --git a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java new file mode 100644 index 000000000000..e9fcf1aa9598 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2020 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.toast; + +import android.animation.Animator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.view.View; +import android.widget.ToastPresenter; + +import com.android.internal.R; +import com.android.systemui.plugins.ToastPlugin; + +/** + * SystemUI TextToast that can be customized by ToastPlugins. Should never instantiate this class + * directly. Instead, use {@link ToastFactory#createToast}. + */ +public class SystemUIToast implements ToastPlugin.Toast { + final Context mContext; + final CharSequence mText; + final ToastPlugin.Toast mPluginToast; + + final int mDefaultGravity; + final int mDefaultY; + final int mDefaultX = 0; + final int mDefaultHorizontalMargin = 0; + final int mDefaultVerticalMargin = 0; + + SystemUIToast(Context context, CharSequence text) { + this(context, text, null); + } + + SystemUIToast(Context context, CharSequence text, ToastPlugin.Toast pluginToast) { + mContext = context; + mText = text; + mPluginToast = pluginToast; + + mDefaultGravity = context.getResources().getInteger(R.integer.config_toastDefaultGravity); + mDefaultY = context.getResources().getDimensionPixelSize(R.dimen.toast_y_offset); + } + + @Override + @NonNull + public Integer getGravity() { + if (isPluginToast() && mPluginToast.getGravity() != null) { + return mPluginToast.getGravity(); + } + return mDefaultGravity; + } + + @Override + @NonNull + public Integer getXOffset() { + if (isPluginToast() && mPluginToast.getXOffset() != null) { + return mPluginToast.getXOffset(); + } + return mDefaultX; + } + + @Override + @NonNull + public Integer getYOffset() { + if (isPluginToast() && mPluginToast.getYOffset() != null) { + return mPluginToast.getYOffset(); + } + return mDefaultY; + } + + @Override + @NonNull + public Integer getHorizontalMargin() { + if (isPluginToast() && mPluginToast.getHorizontalMargin() != null) { + return mPluginToast.getHorizontalMargin(); + } + return mDefaultHorizontalMargin; + } + + @Override + @NonNull + public Integer getVerticalMargin() { + if (isPluginToast() && mPluginToast.getVerticalMargin() != null) { + return mPluginToast.getVerticalMargin(); + } + return mDefaultVerticalMargin; + } + + @Override + @NonNull + public View getView() { + if (isPluginToast() && mPluginToast.getView() != null) { + return mPluginToast.getView(); + } + return ToastPresenter.getTextToastView(mContext, mText); + } + + @Override + @Nullable + public Animator getInAnimation() { + if (isPluginToast() && mPluginToast.getInAnimation() != null) { + return mPluginToast.getInAnimation(); + } + return null; + } + + @Override + @Nullable + public Animator getOutAnimation() { + if (isPluginToast() && mPluginToast.getOutAnimation() != null) { + return mPluginToast.getOutAnimation(); + } + return null; + } + + /** + * Whether this toast has a custom animation. + */ + public boolean hasCustomAnimation() { + return getInAnimation() != null || getOutAnimation() != null; + } + + private boolean isPluginToast() { + return mPluginToast != null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java b/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java new file mode 100644 index 000000000000..d8cb61c6b349 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 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.toast; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.android.systemui.Dumpable; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.plugins.PluginListener; +import com.android.systemui.plugins.ToastPlugin; +import com.android.systemui.shared.plugins.PluginManager; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +import javax.inject.Inject; + +/** + * Factory for creating toasts to be shown by ToastUI. + * These toasts can be customized by {@link ToastPlugin}. + */ +@SysUISingleton +public class ToastFactory implements Dumpable { + // only one ToastPlugin can be connected at a time. + private ToastPlugin mPlugin; + + @Inject + public ToastFactory(PluginManager pluginManager, DumpManager dumpManager) { + dumpManager.registerDumpable("ToastFactory", this); + pluginManager.addPluginListener( + new PluginListener<ToastPlugin>() { + @Override + public void onPluginConnected(ToastPlugin plugin, Context pluginContext) { + mPlugin = plugin; + } + + @Override + public void onPluginDisconnected(ToastPlugin plugin) { + if (plugin.equals(mPlugin)) { + mPlugin = null; + } + } + }, ToastPlugin.class, false /* Allow multiple plugins */); + } + + /** + * Create a toast to be shown by ToastUI. + */ + public SystemUIToast createToast(Context context, CharSequence text, String packageName, + int userId) { + if (isPluginAvailable()) { + return new SystemUIToast(context, text, mPlugin.createToast(text, packageName, userId)); + } + return new SystemUIToast(context, text); + } + + private boolean isPluginAvailable() { + return mPlugin != null; + } + + @Override + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { + pw.println("ToastFactory:"); + pw.println(" mAttachedPlugin=" + mPlugin); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt new file mode 100644 index 000000000000..78173cf62a93 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 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.toast + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.LogLevel.DEBUG +import com.android.systemui.log.LogMessage +import com.android.systemui.log.dagger.ToastLog +import javax.inject.Inject + +private const val TAG = "ToastLog" + +class ToastLogger @Inject constructor( + @ToastLog private val buffer: LogBuffer +) { + + fun logOnShowToast(uid: Int, packageName: String, text: String, token: String) { + log(DEBUG, { + int1 = uid + str1 = packageName + str2 = text + str3 = token + }, { + "[$str3] Show toast for ($str1, $int1). msg=\'$str2\'" + }) + } + + fun logOnHideToast(packageName: String, token: String) { + log(DEBUG, { + str1 = packageName + str2 = token + }, { + "[$str2] Hide toast for [$str1]" + }) + } + + private inline fun log( + logLevel: LogLevel, + initializer: LogMessage.() -> Unit, + noinline printer: LogMessage.() -> String + ) { + buffer.log(TAG, logLevel, initializer, printer) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java index a2203732c47c..1c682e3bb7dc 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java @@ -16,25 +16,27 @@ package com.android.systemui.toast; +import android.animation.Animator; import android.annotation.MainThread; import android.annotation.Nullable; import android.app.INotificationManager; import android.app.ITransientNotificationCallback; import android.content.Context; -import android.content.res.Resources; import android.os.IBinder; import android.os.ServiceManager; import android.os.UserHandle; import android.util.Log; -import android.view.View; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; +import android.widget.Toast; import android.widget.ToastPresenter; -import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.SystemUI; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.Objects; @@ -45,35 +47,53 @@ import javax.inject.Inject; */ @SysUISingleton public class ToastUI extends SystemUI implements CommandQueue.Callbacks { + // values from NotificationManagerService#LONG_DELAY and NotificationManagerService#SHORT_DELAY + private static final int TOAST_LONG_TIME = 3500; // 3.5 seconds + private static final int TOAST_SHORT_TIME = 2000; // 2 seconds + private static final String TAG = "ToastUI"; private final CommandQueue mCommandQueue; private final INotificationManager mNotificationManager; - private final IAccessibilityManager mAccessibilityManager; - private final int mGravity; - private final int mY; + private final IAccessibilityManager mIAccessibilityManager; + private final AccessibilityManager mAccessibilityManager; + private final ToastFactory mToastFactory; + private final DelayableExecutor mMainExecutor; + private final ToastLogger mToastLogger; + private SystemUIToast mToast; @Nullable private ToastPresenter mPresenter; @Nullable private ITransientNotificationCallback mCallback; @Inject - public ToastUI(Context context, CommandQueue commandQueue) { + public ToastUI( + Context context, + CommandQueue commandQueue, + ToastFactory toastFactory, + @Main DelayableExecutor mainExecutor, + ToastLogger toastLogger) { this(context, commandQueue, INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)), IAccessibilityManager.Stub.asInterface( - ServiceManager.getService(Context.ACCESSIBILITY_SERVICE))); + ServiceManager.getService(Context.ACCESSIBILITY_SERVICE)), + toastFactory, + mainExecutor, + toastLogger); } @VisibleForTesting ToastUI(Context context, CommandQueue commandQueue, INotificationManager notificationManager, - @Nullable IAccessibilityManager accessibilityManager) { + @Nullable IAccessibilityManager accessibilityManager, + ToastFactory toastFactory, DelayableExecutor mainExecutor, ToastLogger toastLogger + ) { super(context); mCommandQueue = commandQueue; mNotificationManager = notificationManager; - mAccessibilityManager = accessibilityManager; - Resources resources = mContext.getResources(); - mGravity = resources.getInteger(R.integer.config_toastDefaultGravity); - mY = resources.getDimensionPixelSize(R.dimen.toast_y_offset); + mIAccessibilityManager = accessibilityManager; + mToastFactory = toastFactory; + mMainExecutor = mainExecutor; + mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); + mToastLogger = toastLogger; } @Override @@ -88,12 +108,31 @@ public class ToastUI extends SystemUI implements CommandQueue.Callbacks { if (mPresenter != null) { hideCurrentToast(); } - Context context = mContext.createContextAsUser(UserHandle.getUserHandleForUid(uid), 0); - View view = ToastPresenter.getTextToastView(context, text); + UserHandle userHandle = UserHandle.getUserHandleForUid(uid); + Context context = mContext.createContextAsUser(userHandle, 0); + mToast = mToastFactory.createToast(context, text, packageName, userHandle.getIdentifier()); + + if (mToast.hasCustomAnimation()) { + if (mToast.getInAnimation() != null) { + mToast.getInAnimation().start(); + } + final Animator hideAnimator = mToast.getOutAnimation(); + if (hideAnimator != null) { + final long durationMillis = duration == Toast.LENGTH_LONG + ? TOAST_LONG_TIME : TOAST_SHORT_TIME; + final long updatedDuration = mAccessibilityManager.getRecommendedTimeoutMillis( + (int) durationMillis, AccessibilityManager.FLAG_CONTENT_TEXT); + mMainExecutor.executeDelayed(() -> hideAnimator.start(), + updatedDuration - hideAnimator.getTotalDuration()); + } + } mCallback = callback; - mPresenter = new ToastPresenter(context, mAccessibilityManager, mNotificationManager, + mPresenter = new ToastPresenter(context, mIAccessibilityManager, mNotificationManager, packageName); - mPresenter.show(view, token, windowToken, duration, mGravity, 0, mY, 0, 0, mCallback); + mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString()); + mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(), + mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(), + mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation()); } @Override @@ -104,6 +143,7 @@ public class ToastUI extends SystemUI implements CommandQueue.Callbacks { Log.w(TAG, "Attempt to hide non-current toast from package " + packageName); return; } + mToastLogger.logOnHideToast(packageName, token.toString()); hideCurrentToast(); } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java index 0070dcf9a604..4c724aeea9ae 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java @@ -15,6 +15,7 @@ */ package com.android.systemui.tuner; +import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; @@ -22,6 +23,7 @@ import android.content.DialogInterface; import android.hardware.display.AmbientDisplayConfiguration; import android.os.Build; import android.os.Bundle; +import android.os.UserHandle; import android.provider.Settings; import android.view.Menu; import android.view.MenuInflater; @@ -122,7 +124,8 @@ public class TunerFragment extends PreferenceFragment { getActivity().finish(); return true; case MENU_REMOVE: - TunerService.showResetRequest(getContext(), new Runnable() { + UserHandle user = new UserHandle(ActivityManager.getCurrentUser()); + TunerService.showResetRequest(getContext(), user, new Runnable() { @Override public void run() { if (getActivity() != null) { diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java index 338e1781abd0..70bba263ab90 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java @@ -14,7 +14,6 @@ package com.android.systemui.tuner; -import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -51,25 +50,24 @@ public abstract class TunerService { void onTuningChanged(String key, String newValue); } - private static Context userContext(Context context) { + private static Context userContext(Context context, UserHandle user) { try { - return context.createPackageContextAsUser(context.getPackageName(), 0, - new UserHandle(ActivityManager.getCurrentUser())); + return context.createPackageContextAsUser(context.getPackageName(), 0, user); } catch (NameNotFoundException e) { return context; } } - public static final void setTunerEnabled(Context context, boolean enabled) { - userContext(context).getPackageManager().setComponentEnabledSetting( + public static final void setTunerEnabled(Context context, UserHandle user, boolean enabled) { + userContext(context, user).getPackageManager().setComponentEnabledSetting( new ComponentName(context, TunerActivity.class), enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } - public static final boolean isTunerEnabled(Context context) { - return userContext(context).getPackageManager().getComponentEnabledSetting( + public static final boolean isTunerEnabled(Context context, UserHandle user) { + return userContext(context, user).getPackageManager().getComponentEnabledSetting( new ComponentName(context, TunerActivity.class)) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; } @@ -83,7 +81,8 @@ public abstract class TunerService { } } - public static final void showResetRequest(final Context context, final Runnable onDisabled) { + public static final void showResetRequest(final Context context, UserHandle user, + final Runnable onDisabled) { SystemUIDialog dialog = new SystemUIDialog(context); dialog.setShowForAllUsers(true); dialog.setMessage(R.string.remove_from_settings_prompt); @@ -91,20 +90,20 @@ public abstract class TunerService { (OnClickListener) null); dialog.setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.guest_exit_guest_dialog_remove), new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // Tell the tuner (in main SysUI process) to clear all its settings. - context.sendBroadcast(new Intent(TunerService.ACTION_CLEAR)); - // Disable access to tuner. - TunerService.setTunerEnabled(context, false); - // Make them sit through the warning dialog again. - Settings.Secure.putInt(context.getContentResolver(), - TunerFragment.SETTING_SEEN_TUNER_WARNING, 0); - if (onDisabled != null) { - onDisabled.run(); - } - } - }); + @Override + public void onClick(DialogInterface dialog, int which) { + // Tell the tuner (in main SysUI process) to clear all its settings. + context.sendBroadcast(new Intent(TunerService.ACTION_CLEAR)); + // Disable access to tuner. + TunerService.setTunerEnabled(context, user, false); + // Make them sit through the warning dialog again. + Settings.Secure.putInt(context.getContentResolver(), + TunerFragment.SETTING_SEEN_TUNER_WARNING, 0); + if (onDisabled != null) { + onDisabled.run(); + } + } + }); dialog.show(); } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index d9727a73f651..22f03e074b06 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -15,13 +15,13 @@ */ package com.android.systemui.tuner; -import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.Looper; import android.os.UserManager; import android.provider.Settings; @@ -37,7 +37,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.qs.QSTileHost; -import com.android.systemui.settings.CurrentUserTracker; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.util.leak.LeakDetector; @@ -81,7 +81,8 @@ public class TunerServiceImpl extends TunerService { private ContentResolver mContentResolver; private int mCurrentUser; - private CurrentUserTracker mUserTracker; + private UserTracker.Callback mCurrentUserTracker; + private UserTracker mUserTracker; /** */ @@ -91,11 +92,13 @@ public class TunerServiceImpl extends TunerService { @Main Handler mainHandler, LeakDetector leakDetector, DemoModeController demoModeController, - BroadcastDispatcher broadcastDispatcher) { + BroadcastDispatcher broadcastDispatcher, + UserTracker userTracker) { mContext = context; mContentResolver = mContext.getContentResolver(); mLeakDetector = leakDetector; mDemoModeController = demoModeController; + mUserTracker = userTracker; for (UserInfo user : UserManager.get(mContext).getUsers()) { mCurrentUser = user.getUserHandle().getIdentifier(); @@ -104,21 +107,22 @@ public class TunerServiceImpl extends TunerService { } } - mCurrentUser = ActivityManager.getCurrentUser(); - mUserTracker = new CurrentUserTracker(broadcastDispatcher) { + mCurrentUser = mUserTracker.getUserId(); + mCurrentUserTracker = new UserTracker.Callback() { @Override - public void onUserSwitched(int newUserId) { - mCurrentUser = newUserId; + public void onUserChanged(int newUser, Context userContext) { + mCurrentUser = newUser; reloadAll(); reregisterAll(); } }; - mUserTracker.startTracking(); + mUserTracker.addCallback(mCurrentUserTracker, + new HandlerExecutor(mainHandler)); } @Override public void destroy() { - mUserTracker.stopTracking(); + mUserTracker.removeCallback(mCurrentUserTracker); } private void upgradeTuner(int oldVersion, int newVersion, Handler mainHandler) { @@ -137,7 +141,7 @@ public class TunerServiceImpl extends TunerService { } } if (oldVersion < 2) { - setTunerEnabled(mContext, false); + setTunerEnabled(mContext, mUserTracker.getUserHandle(), false); } // 3 Removed because of a revert. if (oldVersion < 4) { @@ -272,7 +276,7 @@ public class TunerServiceImpl extends TunerService { @Override public void onChange(boolean selfChange, java.util.Collection<Uri> uris, int flags, int userId) { - if (userId == ActivityManager.getCurrentUser()) { + if (userId == mUserTracker.getUserId()) { for (Uri u : uris) { reloadSetting(u); } diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java index 22fa0106795a..bde88b1b5533 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIBinder.java @@ -17,11 +17,12 @@ package com.android.systemui.tv; import com.android.systemui.dagger.GlobalRootComponent; +import com.android.systemui.wmshell.TvPipModule; import dagger.Binds; import dagger.Module; -@Module() +@Module(includes = TvPipModule.class) interface TvSystemUIBinder { @Binds GlobalRootComponent bindGlobalRootComponent(TvGlobalRootComponent globalRootComponent); diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java new file mode 100644 index 000000000000..890ee5f45309 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2020 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.user; + +import android.app.Activity; +import android.app.Dialog; +import android.app.IActivityManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.users.EditUserInfoController; +import com.android.systemui.R; + +import javax.inject.Inject; + +/** + * This screen shows a Dialog for choosing nickname and photo for a new user, and then delegates the + * user creation to a UserCreator. + */ +public class CreateUserActivity extends Activity { + + /** + * Creates an intent to start this activity. + */ + public static Intent createIntentForStart(Context context) { + return new Intent(context, CreateUserActivity.class); + } + + private static final String TAG = "CreateUserActivity"; + private static final String DIALOG_STATE_KEY = "create_user_dialog_state"; + + private final UserCreator mUserCreator; + private final EditUserInfoController mEditUserInfoController; + private final IActivityManager mActivityManager; + + private Dialog mSetupUserDialog; + + @Inject + public CreateUserActivity(UserCreator userCreator, + EditUserInfoController editUserInfoController, IActivityManager activityManager) { + mUserCreator = userCreator; + mEditUserInfoController = editUserInfoController; + mActivityManager = activityManager; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setShowWhenLocked(true); + setContentView(R.layout.activity_create_new_user); + + if (savedInstanceState != null) { + mEditUserInfoController.onRestoreInstanceState(savedInstanceState); + } + + mSetupUserDialog = createDialog(); + mSetupUserDialog.show(); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + if (mSetupUserDialog != null && mSetupUserDialog.isShowing()) { + outState.putBundle(DIALOG_STATE_KEY, mSetupUserDialog.onSaveInstanceState()); + } + + mEditUserInfoController.onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + Bundle savedDialogState = savedInstanceState.getBundle(DIALOG_STATE_KEY); + if (savedDialogState != null && mSetupUserDialog != null) { + mSetupUserDialog.onRestoreInstanceState(savedDialogState); + } + } + + private Dialog createDialog() { + String defaultUserName = getString(com.android.settingslib.R.string.user_new_user_name); + + return mEditUserInfoController.createDialog( + this, + (intent, requestCode) -> { + mEditUserInfoController.startingActivityForResult(); + startActivityForResult(intent, requestCode); + }, + null, + defaultUserName, + getString(R.string.user_add_user), + this::addUserNow, + this::finish + ); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + mEditUserInfoController.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + if (mSetupUserDialog != null) { + mSetupUserDialog.dismiss(); + } + } + + private void addUserNow(String userName, Drawable userIcon) { + mSetupUserDialog.dismiss(); + + userName = (userName == null || userName.trim().isEmpty()) + ? getString(R.string.user_new_user_name) + : userName; + + mUserCreator.createUser(userName, userIcon, + userInfo -> { + switchToUser(userInfo.id); + finishIfNeeded(); + }, () -> { + Log.e(TAG, "Unable to create user"); + finishIfNeeded(); + }); + } + + private void finishIfNeeded() { + if (!isFinishing() && !isDestroyed()) { + finish(); + } + } + + private void switchToUser(int userId) { + try { + mActivityManager.switchUser(userId); + } catch (RemoteException e) { + Log.e(TAG, "Couldn't switch user.", e); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.java b/packages/SystemUI/src/com/android/systemui/user/UserCreator.java new file mode 100644 index 000000000000..3a270bb77e46 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 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.user; + +import android.app.Dialog; +import android.content.Context; +import android.content.pm.UserInfo; +import android.graphics.drawable.Drawable; +import android.os.UserManager; + +import com.android.internal.util.UserIcons; +import com.android.settingslib.users.UserCreatingDialog; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.function.Consumer; + +import javax.inject.Inject; + +/** + * A class to do the user creation process. It shows a progress dialog, and manages the user + * creation + */ +public class UserCreator { + + private final Context mContext; + private final UserManager mUserManager; + + @Inject + public UserCreator(Context context, UserManager userManager) { + mContext = context; + mUserManager = userManager; + } + + /** + * Shows a progress dialog then starts the user creation process on the main thread. + * + * @param successCallback is called when the user creation is successful. + * @param errorCallback is called when userManager.createUser returns null. + * (Exceptions are not handled by this class) + */ + public void createUser(String userName, Drawable userIcon, Consumer<UserInfo> successCallback, + Runnable errorCallback) { + + Dialog userCreationProgressDialog = new UserCreatingDialog(mContext); + userCreationProgressDialog.show(); + + // userManager.createUser will block the thread so post is needed for the dialog to show + ThreadUtils.postOnMainThread(() -> { + UserInfo user = + mUserManager.createUser(userName, UserManager.USER_TYPE_FULL_SECONDARY, 0); + if (user == null) { + // Couldn't create user for some reason + userCreationProgressDialog.dismiss(); + errorCallback.run(); + return; + } + + Drawable newUserIcon = userIcon; + if (newUserIcon == null) { + newUserIcon = UserIcons.getDefaultUserIcon(mContext.getResources(), user.id, false); + } + mUserManager.setUserIcon(user.id, UserIcons.convertToBitmap(newUserIcon)); + + userCreationProgressDialog.dismiss(); + successCallback.accept(user); + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java new file mode 100644 index 000000000000..0ad0984e8231 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 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.user; + +import com.android.settingslib.users.EditUserInfoController; + +import dagger.Module; +import dagger.Provides; + +/** + * Dagger module for User related classes. + */ +@Module +public class UserModule { + + private static final String FILE_PROVIDER_AUTHORITY = "com.android.systemui.fileprovider"; + + @Provides + EditUserInfoController provideEditUserInfoController() { + return new EditUserInfoController(FILE_PROVIDER_AUTHORITY); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java deleted file mode 100644 index 8946c97a4b58..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2020 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.util; - -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.view.Gravity; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import com.android.systemui.R; - -/** - * Circular view with a semitransparent, circular background with an 'X' inside it. - * - * This is used by both Bubbles and PIP as the dismiss target. - */ -public class DismissCircleView extends FrameLayout { - - private final ImageView mIconView = new ImageView(getContext()); - - public DismissCircleView(Context context) { - super(context); - final Resources res = getResources(); - - setBackground(res.getDrawable(R.drawable.dismiss_circle_background)); - - mIconView.setImageDrawable(res.getDrawable(R.drawable.pip_ic_close_white)); - addView(mIconView); - - setViewSizes(); - } - - @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - setViewSizes(); - } - - /** Retrieves the current dimensions for the icon and circle and applies them. */ - private void setViewSizes() { - final Resources res = getResources(); - final int iconSize = res.getDimensionPixelSize(R.dimen.dismiss_target_x_size); - mIconView.setLayoutParams( - new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER)); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/FloatingContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/util/FloatingContentCoordinator.kt deleted file mode 100644 index bcfb2afeeda1..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/FloatingContentCoordinator.kt +++ /dev/null @@ -1,350 +0,0 @@ -package com.android.systemui.util - -import android.graphics.Rect -import android.util.Log -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.util.FloatingContentCoordinator.FloatingContent -import java.util.HashMap - -/** Tag for debug logging. */ -private const val TAG = "FloatingCoordinator" - -/** - * Coordinates the positions and movement of floating content, such as PIP and Bubbles, to ensure - * that they don't overlap. If content does overlap due to content appearing or moving, the - * coordinator will ask content to move to resolve the conflict. - * - * After implementing [FloatingContent], content should call [onContentAdded] to begin coordination. - * Subsequently, call [onContentMoved] whenever the content moves, and the coordinator will move - * other content out of the way. [onContentRemoved] should be called when the content is removed or - * no longer visible. - */ - -@SysUISingleton -class FloatingContentCoordinator constructor() { - /** - * Represents a piece of floating content, such as PIP or the Bubbles stack. Provides methods - * that allow the [FloatingContentCoordinator] to determine the current location of the content, - * as well as the ability to ask it to move out of the way of other content. - * - * The default implementation of [calculateNewBoundsOnOverlap] moves the content up or down, - * depending on the position of the conflicting content. You can override this method if you - * want your own custom conflict resolution logic. - */ - interface FloatingContent { - - /** - * Return the bounds claimed by this content. This should include the bounds occupied by the - * content itself, as well as any padding, if desired. The coordinator will ensure that no - * other content is located within these bounds. - * - * If the content is animating, this method should return the bounds to which the content is - * animating. If that animation is cancelled, or updated, be sure that your implementation - * of this method returns the appropriate bounds, and call [onContentMoved] so that the - * coordinator moves other content out of the way. - */ - fun getFloatingBoundsOnScreen(): Rect - - /** - * Return the area within which this floating content is allowed to move. When resolving - * conflicts, the coordinator will never ask your content to move to a position where any - * part of the content would be out of these bounds. - */ - fun getAllowedFloatingBoundsRegion(): Rect - - /** - * Called when the coordinator needs this content to move to the given bounds. It's up to - * you how to do that. - * - * Note that if you start an animation to these bounds, [getFloatingBoundsOnScreen] should - * return the destination bounds, not the in-progress animated bounds. This is so the - * coordinator knows where floating content is going to be and can resolve conflicts - * accordingly. - */ - fun moveToBounds(bounds: Rect) - - /** - * Called by the coordinator when it needs to find a new home for this floating content, - * because a new or moving piece of content is now overlapping with it. - * - * [findAreaForContentVertically] and [findAreaForContentAboveOrBelow] are helpful utility - * functions that will find new bounds for your content automatically. Unless you require - * specific conflict resolution logic, these should be sufficient. By default, this method - * delegates to [findAreaForContentVertically]. - * - * @param overlappingContentBounds The bounds of the other piece of content, which - * necessitated this content's relocation. Your new position must not overlap with these - * bounds. - * @param otherContentBounds The bounds of any other pieces of floating content. Your new - * position must not overlap with any of these either. These bounds are guaranteed to be - * non-overlapping. - * @return The new bounds for this content. - */ - @JvmDefault - fun calculateNewBoundsOnOverlap( - overlappingContentBounds: Rect, - otherContentBounds: List<Rect> - ): Rect { - return findAreaForContentVertically( - getFloatingBoundsOnScreen(), - overlappingContentBounds, - otherContentBounds, - getAllowedFloatingBoundsRegion()) - } - } - - /** The bounds of all pieces of floating content added to the coordinator. */ - private val allContentBounds: MutableMap<FloatingContent, Rect> = HashMap() - - /** - * Whether we are currently resolving conflicts by asking content to move. If we are, we'll - * temporarily ignore calls to [onContentMoved] - those calls are from the content that is - * moving to new, conflict-free bounds, so we don't need to perform conflict detection - * calculations in response. - */ - private var currentlyResolvingConflicts = false - - /** - * Makes the coordinator aware of a new piece of floating content, and moves any existing - * content out of the way, if necessary. - * - * If you don't want your new content to move existing content, use [getOccupiedBounds] to find - * an unoccupied area, and move the content there before calling this method. - */ - fun onContentAdded(newContent: FloatingContent) { - updateContentBounds() - allContentBounds[newContent] = newContent.getFloatingBoundsOnScreen() - maybeMoveConflictingContent(newContent) - } - - /** - * Called to notify the coordinator that a piece of floating content has moved (or is animating) - * to a new position, and that any conflicting floating content should be moved out of the way. - * - * The coordinator will call [FloatingContent.getFloatingBoundsOnScreen] to find the new bounds - * for the moving content. If you're animating the content, be sure that your implementation of - * getFloatingBoundsOnScreen returns the bounds to which it's animating, not the content's - * current bounds. - * - * If the animation moving this content is cancelled or updated, you'll need to call this method - * again, to ensure that content is moved out of the way of the latest bounds. - * - * @param content The content that has moved. - */ - fun onContentMoved(content: FloatingContent) { - - // Ignore calls when we are currently resolving conflicts, since those calls are from - // content that is moving to new, conflict-free bounds. - if (currentlyResolvingConflicts) { - return - } - - if (!allContentBounds.containsKey(content)) { - Log.wtf(TAG, "Received onContentMoved call before onContentAdded! " + - "This should never happen.") - return - } - - updateContentBounds() - maybeMoveConflictingContent(content) - } - - /** - * Called to notify the coordinator that a piece of floating content has been removed or is no - * longer visible. - */ - fun onContentRemoved(removedContent: FloatingContent) { - allContentBounds.remove(removedContent) - } - - /** - * Returns a set of Rects that represent the bounds of all of the floating content on the - * screen. - * - * [onContentAdded] will move existing content out of the way if the added content intersects - * existing content. That's fine - but if your specific starting position is not important, you - * can use this function to find unoccupied space for your content before calling - * [onContentAdded], so that moving existing content isn't necessary. - */ - fun getOccupiedBounds(): Collection<Rect> { - return allContentBounds.values - } - - /** - * Identifies any pieces of content that are now overlapping with the given content, and asks - * them to move out of the way. - */ - private fun maybeMoveConflictingContent(fromContent: FloatingContent) { - currentlyResolvingConflicts = true - - val conflictingNewBounds = allContentBounds[fromContent]!! - allContentBounds - // Filter to content that intersects with the new bounds. That's content that needs - // to move. - .filter { (content, bounds) -> - content != fromContent && Rect.intersects(conflictingNewBounds, bounds) } - // Tell that content to get out of the way, and save the bounds it says it's moving - // (or animating) to. - .forEach { (content, bounds) -> - val newBounds = content.calculateNewBoundsOnOverlap( - conflictingNewBounds, - // Pass all of the content bounds except the bounds of the - // content we're asking to move, and the conflicting new bounds - // (since those are passed separately). - otherContentBounds = allContentBounds.values - .minus(bounds) - .minus(conflictingNewBounds)) - - // If the new bounds are empty, it means there's no non-overlapping position - // that is in bounds. Just leave the content where it is. This should normally - // not happen, but sometimes content like PIP reports incorrect bounds - // temporarily. - if (!newBounds.isEmpty) { - content.moveToBounds(newBounds) - allContentBounds[content] = content.getFloatingBoundsOnScreen() - } - } - - currentlyResolvingConflicts = false - } - - /** - * Update [allContentBounds] by calling [FloatingContent.getFloatingBoundsOnScreen] for all - * content and saving the result. - */ - private fun updateContentBounds() { - allContentBounds.keys.forEach { allContentBounds[it] = it.getFloatingBoundsOnScreen() } - } - - companion object { - /** - * Finds new bounds for the given content, either above or below its current position. The - * new bounds won't intersect with the newly overlapping rect or the exclusion rects, and - * will be within the allowed bounds unless no possible position exists. - * - * You can use this method to help find a new position for your content when the coordinator - * calls [FloatingContent.moveToAreaExcluding]. - * - * @param contentRect The bounds of the content for which we're finding a new home. - * @param newlyOverlappingRect The bounds of the content that forced this relocation by - * intersecting with the content we now need to move. If the overlapping content is - * overlapping the top half of this content, we'll try to move this content downward if - * possible (since the other content is 'pushing' it down), and vice versa. - * @param exclusionRects Any other areas that we need to avoid when finding a new home for - * the content. These areas must be non-overlapping with each other. - * @param allowedBounds The area within which we're allowed to find new bounds for the - * content. - * @return New bounds for the content that don't intersect the exclusion rects or the - * newly overlapping rect, and that is within bounds - or an empty Rect if no in-bounds - * position exists. - */ - @JvmStatic - fun findAreaForContentVertically( - contentRect: Rect, - newlyOverlappingRect: Rect, - exclusionRects: Collection<Rect>, - allowedBounds: Rect - ): Rect { - // If the newly overlapping Rect's center is above the content's center, we'll prefer to - // find a space for this content that is below the overlapping content, since it's - // 'pushing' it down. This may not be possible due to to screen bounds, in which case - // we'll find space in the other direction. - val overlappingContentPushingDown = - newlyOverlappingRect.centerY() < contentRect.centerY() - - // Filter to exclusion rects that are above or below the content that we're finding a - // place for. Then, split into two lists - rects above the content, and rects below it. - var (rectsToAvoidAbove, rectsToAvoidBelow) = exclusionRects - .filter { rectToAvoid -> rectsIntersectVertically(rectToAvoid, contentRect) } - .partition { rectToAvoid -> rectToAvoid.top < contentRect.top } - - // Lazily calculate the closest possible new tops for the content, above and below its - // current location. - val newContentBoundsAbove by lazy { findAreaForContentAboveOrBelow( - contentRect, - exclusionRects = rectsToAvoidAbove.plus(newlyOverlappingRect), - findAbove = true) } - val newContentBoundsBelow by lazy { findAreaForContentAboveOrBelow( - contentRect, - exclusionRects = rectsToAvoidBelow.plus(newlyOverlappingRect), - findAbove = false) } - - val positionAboveInBounds by lazy { allowedBounds.contains(newContentBoundsAbove) } - val positionBelowInBounds by lazy { allowedBounds.contains(newContentBoundsBelow) } - - // Use the 'below' position if the content is being overlapped from the top, unless it's - // out of bounds. Also use it if the content is being overlapped from the bottom, but - // the 'above' position is out of bounds. Otherwise, use the 'above' position. - val usePositionBelow = - overlappingContentPushingDown && positionBelowInBounds || - !overlappingContentPushingDown && !positionAboveInBounds - - // Return the content rect, but offset to reflect the new position. - val newBounds = if (usePositionBelow) newContentBoundsBelow else newContentBoundsAbove - - // If the new bounds are within the allowed bounds, return them. If not, it means that - // there are no legal new bounds. This can happen if the new content's bounds are too - // large (for example, full-screen PIP). Since there is no reasonable action to take - // here, return an empty Rect and we will just not move the content. - return if (allowedBounds.contains(newBounds)) newBounds else Rect() - } - - /** - * Finds a new position for the given content, either above or below its current position - * depending on whether [findAbove] is true or false, respectively. This new position will - * not intersect with any of the [exclusionRects]. - * - * This method is useful as a helper method for implementing your own conflict resolution - * logic. Otherwise, you'd want to use [findAreaForContentVertically], which takes screen - * bounds and conflicting bounds' location into account when deciding whether to move to new - * bounds above or below the current bounds. - * - * @param contentRect The content we're finding an area for. - * @param exclusionRects The areas we need to avoid when finding a new area for the content. - * These areas must be non-overlapping with each other. - * @param findAbove Whether we are finding an area above the content's current position, - * rather than an area below it. - */ - fun findAreaForContentAboveOrBelow( - contentRect: Rect, - exclusionRects: Collection<Rect>, - findAbove: Boolean - ): Rect { - // Sort the rects, since we want to move the content as little as possible. We'll - // start with the rects closest to the content and move outward. If we're finding an - // area above the content, that means we sort in reverse order to search the rects - // from highest to lowest y-value. - val sortedExclusionRects = - exclusionRects.sortedBy { if (findAbove) -it.top else it.top } - - val proposedNewBounds = Rect(contentRect) - for (exclusionRect in sortedExclusionRects) { - // If the proposed new bounds don't intersect with this exclusion rect, that - // means there's room for the content here. We know this because the rects are - // sorted and non-overlapping, so any subsequent exclusion rects would be higher - // (or lower) than this one and can't possibly intersect if this one doesn't. - if (!Rect.intersects(proposedNewBounds, exclusionRect)) { - break - } else { - // Otherwise, we need to keep searching for new bounds. If we're finding an - // area above, propose new bounds that place the content just above the - // exclusion rect. If we're finding an area below, propose new bounds that - // place the content just below the exclusion rect. - val verticalOffset = - if (findAbove) -contentRect.height() else exclusionRect.height() - proposedNewBounds.offsetTo( - proposedNewBounds.left, - exclusionRect.top + verticalOffset) - } - } - - return proposedNewBounds - } - - /** Returns whether or not the two Rects share any of the same space on the X axis. */ - private fun rectsIntersectVertically(r1: Rect, r2: Rect): Boolean { - return (r1.left >= r2.left && r1.left <= r2.right) || - (r1.right <= r2.right && r1.right >= r2.left) - } - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java b/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java index a6cd350b33ce..344f0d2f5506 100644 --- a/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java +++ b/packages/SystemUI/src/com/android/systemui/util/InjectionInflationController.java @@ -27,7 +27,6 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.qs.QSFooterImpl; import com.android.systemui.qs.QSPanel; import com.android.systemui.qs.QuickQSPanel; -import com.android.systemui.qs.QuickStatusBarHeader; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; @@ -93,10 +92,6 @@ public class InjectionInflationController { } /** - * Creates the QuickStatusBarHeader. - */ - QuickStatusBarHeader createQsHeader(); - /** * Creates the QSFooterImpl. */ QSFooterImpl createQsFooter(); diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java index 90b95ea9562e..0d63324966fd 100644 --- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java +++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java @@ -25,6 +25,7 @@ import android.provider.Settings; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; import com.android.systemui.SystemUI; +import com.android.wm.shell.pip.tv.PipNotification; import java.util.Arrays; @@ -34,8 +35,8 @@ public class NotificationChannels extends SystemUI { public static String SCREENSHOTS_HEADSUP = "SCN_HEADSUP"; public static String GENERAL = "GEN"; public static String STORAGE = "DSK"; - public static String TVPIP = "TPP"; public static String BATTERY = "BAT"; + public static String TVPIP = PipNotification.NOTIFICATION_CHANNEL_TVPIP; public static String HINTS = "HNT"; public NotificationChannels(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/FloatProperties.kt b/packages/SystemUI/src/com/android/systemui/util/animation/FloatProperties.kt deleted file mode 100644 index a284a747da21..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/animation/FloatProperties.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2020 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.util.animation - -import android.graphics.Rect -import android.graphics.RectF -import androidx.dynamicanimation.animation.FloatPropertyCompat - -/** - * Helpful extra properties to use with the [PhysicsAnimator]. These allow you to animate objects - * such as [Rect] and [RectF]. - * - * There are additional, more basic properties available in [DynamicAnimation]. - */ -class FloatProperties { - companion object { - /** - * Represents the x-coordinate of a [Rect]. Typically used to animate moving a Rect - * horizontally. - * - * This property's getter returns [Rect.left], and its setter uses [Rect.offsetTo], which - * sets [Rect.left] to the new value and offsets [Rect.right] so that the width of the Rect - * does not change. - */ - @JvmField - val RECT_X = object : FloatPropertyCompat<Rect>("RectX") { - override fun setValue(rect: Rect?, value: Float) { - rect?.offsetTo(value.toInt(), rect.top) - } - - override fun getValue(rect: Rect?): Float { - return rect?.left?.toFloat() ?: -Float.MAX_VALUE - } - } - - /** - * Represents the y-coordinate of a [Rect]. Typically used to animate moving a Rect - * vertically. - * - * This property's getter returns [Rect.top], and its setter uses [Rect.offsetTo], which - * sets [Rect.top] to the new value and offsets [Rect.bottom] so that the height of the Rect - * does not change. - */ - @JvmField - val RECT_Y = object : FloatPropertyCompat<Rect>("RectY") { - override fun setValue(rect: Rect?, value: Float) { - rect?.offsetTo(rect.left, value.toInt()) - } - - override fun getValue(rect: Rect?): Float { - return rect?.top?.toFloat() ?: -Float.MAX_VALUE - } - } - - /** - * Represents the width of a [Rect]. Typically used to animate resizing a Rect horizontally. - * - * This property's getter returns [Rect.width], and its setter changes the value of - * [Rect.right] by adding the animated width value to [Rect.left]. - */ - @JvmField - val RECT_WIDTH = object : FloatPropertyCompat<Rect>("RectWidth") { - override fun getValue(rect: Rect): Float { - return rect.width().toFloat() - } - - override fun setValue(rect: Rect, value: Float) { - rect.right = rect.left + value.toInt() - } - } - - /** - * Represents the height of a [Rect]. Typically used to animate resizing a Rect vertically. - * - * This property's getter returns [Rect.height], and its setter changes the value of - * [Rect.bottom] by adding the animated height value to [Rect.top]. - */ - @JvmField - val RECT_HEIGHT = object : FloatPropertyCompat<Rect>("RectHeight") { - override fun getValue(rect: Rect): Float { - return rect.height().toFloat() - } - - override fun setValue(rect: Rect, value: Float) { - rect.bottom = rect.top + value.toInt() - } - } - - /** - * Represents the x-coordinate of a [RectF]. Typically used to animate moving a RectF - * horizontally. - * - * This property's getter returns [RectF.left], and its setter uses [RectF.offsetTo], which - * sets [RectF.left] to the new value and offsets [RectF.right] so that the width of the - * RectF does not change. - */ - @JvmField - val RECTF_X = object : FloatPropertyCompat<RectF>("RectFX") { - override fun setValue(rect: RectF?, value: Float) { - rect?.offsetTo(value, rect.top) - } - - override fun getValue(rect: RectF?): Float { - return rect?.left ?: -Float.MAX_VALUE - } - } - - /** - * Represents the y-coordinate of a [RectF]. Typically used to animate moving a RectF - * vertically. - * - * This property's getter returns [RectF.top], and its setter uses [RectF.offsetTo], which - * sets [RectF.top] to the new value and offsets [RectF.bottom] so that the height of the - * RectF does not change. - */ - @JvmField - val RECTF_Y = object : FloatPropertyCompat<RectF>("RectFY") { - override fun setValue(rect: RectF?, value: Float) { - rect?.offsetTo(rect.left, value) - } - - override fun getValue(rect: RectF?): Float { - return rect?.top ?: -Float.MAX_VALUE - } - } - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt deleted file mode 100644 index 2a5424ce4ef7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt +++ /dev/null @@ -1,1071 +0,0 @@ -/* - * Copyright (C) 2019 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.util.animation - -import android.os.Looper -import android.util.ArrayMap -import android.util.Log -import android.view.View -import androidx.dynamicanimation.animation.AnimationHandler -import androidx.dynamicanimation.animation.DynamicAnimation -import androidx.dynamicanimation.animation.FlingAnimation -import androidx.dynamicanimation.animation.FloatPropertyCompat -import androidx.dynamicanimation.animation.SpringAnimation -import androidx.dynamicanimation.animation.SpringForce -import com.android.systemui.util.animation.PhysicsAnimator.Companion.getInstance -import java.lang.ref.WeakReference -import java.util.WeakHashMap -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min - -/** - * Extension function for all objects which will return a PhysicsAnimator instance for that object. - */ -val <T : View> T.physicsAnimator: PhysicsAnimator<T> get() { return getInstance(this) } - -private const val TAG = "PhysicsAnimator" - -private val UNSET = -Float.MAX_VALUE - -/** - * [FlingAnimation] multiplies the friction set via [FlingAnimation.setFriction] by 4.2f, which is - * where this number comes from. We use it in [PhysicsAnimator.flingThenSpring] to calculate the - * minimum velocity for a fling to reach a certain value, given the fling's friction. - */ -private const val FLING_FRICTION_SCALAR_MULTIPLIER = 4.2f - -typealias EndAction = () -> Unit - -/** A map of Property -> AnimationUpdate, which is provided to update listeners on each frame. */ -typealias UpdateMap<T> = - ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> - -/** - * Map of the animators associated with a given object. This ensures that only one animator - * per object exists. - */ -internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>() - -/** - * Default spring configuration to use for animations where stiffness and/or damping ratio - * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. - */ -private val globalDefaultSpring = PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_MEDIUM, - SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) - -/** - * Default fling configuration to use for animations where friction was not provided, and a default - * fling config was not set via [PhysicsAnimator.setDefaultFlingConfig]. - */ -private val globalDefaultFling = PhysicsAnimator.FlingConfig( - friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE) - -/** Whether to log helpful debug information about animations. */ -private var verboseLogging = false - -/** - * Animator that uses physics-based animations to animate properties on views and objects. Physics - * animations use real-world physical concepts, such as momentum and mass, to realistically simulate - * motion. PhysicsAnimator is heavily inspired by [android.view.ViewPropertyAnimator], and - * also uses the builder pattern to configure and start animations. - * - * The physics animations are backed by [DynamicAnimation]. - * - * @param T The type of the object being animated. - */ -class PhysicsAnimator<T> private constructor (target: T) { - /** Weak reference to the animation target. */ - val weakTarget = WeakReference(target) - - /** Data class for representing animation frame updates. */ - data class AnimationUpdate(val value: Float, val velocity: Float) - - /** [DynamicAnimation] instances for the given properties. */ - private val springAnimations = ArrayMap<FloatPropertyCompat<in T>, SpringAnimation>() - private val flingAnimations = ArrayMap<FloatPropertyCompat<in T>, FlingAnimation>() - - /** - * Spring and fling configurations for the properties to be animated on the target. We'll - * configure and start the DynamicAnimations for these properties according to the provided - * configurations. - */ - private val springConfigs = ArrayMap<FloatPropertyCompat<in T>, SpringConfig>() - private val flingConfigs = ArrayMap<FloatPropertyCompat<in T>, FlingConfig>() - - /** - * Animation listeners for the animation. These will be notified when each property animation - * updates or ends. - */ - private val updateListeners = ArrayList<UpdateListener<T>>() - private val endListeners = ArrayList<EndListener<T>>() - - /** End actions to run when all animations have completed. */ - private val endActions = ArrayList<EndAction>() - - /** SpringConfig to use by default for properties whose springs were not provided. */ - private var defaultSpring: SpringConfig = globalDefaultSpring - - /** FlingConfig to use by default for properties whose fling configs were not provided. */ - private var defaultFling: FlingConfig = globalDefaultFling - - /** - * AnimationHandler to use if it need custom AnimationHandler, if this is null, it will use - * the default AnimationHandler in the DynamicAnimation. - */ - private var customAnimationHandler: AnimationHandler? = null - - /** - * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to - * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add - * just one permanent update and end listener to the DynamicAnimations. - */ - internal var internalListeners = ArrayList<InternalListener>() - - /** - * Action to run when [start] is called. This can be changed by - * [PhysicsAnimatorTestUtils.prepareForTest] to enable animators to run under test and provide - * helpful test utilities. - */ - internal var startAction: () -> Unit = ::startInternal - - /** - * Action to run when [cancel] is called. This can be changed by - * [PhysicsAnimatorTestUtils.prepareForTest] to cancel animations from the main thread, which - * is required. - */ - internal var cancelAction: (Set<FloatPropertyCompat<in T>>) -> Unit = ::cancelInternal - - /** - * Springs a property to the given value, using the provided configuration settings. - * - * Springs are used when you know the exact value to which you want to animate. They can be - * configured with a start velocity (typically used when the spring is initiated by a touch - * event), but this velocity will be realistically attenuated as forces are applied to move the - * property towards the end value. - * - * If you find yourself repeating the same stiffness and damping ratios many times, consider - * storing a single [SpringConfig] instance and passing that in instead of individual values. - * - * @param property The property to spring to the given value. The property must be an instance - * of FloatPropertyCompat<? super T>. For example, if this is a - * PhysicsAnimator<FrameLayout>, you can use a FloatPropertyCompat<FrameLayout>, as - * well as a FloatPropertyCompat<ViewGroup>, and so on. - * @param toPosition The value to spring the given property to. - * @param startVelocity The initial velocity to use for the animation. - * @param stiffness The stiffness to use for the spring. Higher stiffness values result in - * faster animations, while lower stiffness means a slower animation. Reasonable values for - * low, medium, and high stiffness can be found as constants in [SpringForce]. - * @param dampingRatio The damping ratio (bounciness) to use for the spring. Higher values - * result in a less 'springy' animation, while lower values allow the animation to bounce - * back and forth for a longer time after reaching the final position. Reasonable values for - * low, medium, and high damping can be found in [SpringForce]. - */ - fun spring( - property: FloatPropertyCompat<in T>, - toPosition: Float, - startVelocity: Float = 0f, - stiffness: Float = defaultSpring.stiffness, - dampingRatio: Float = defaultSpring.dampingRatio - ): PhysicsAnimator<T> { - if (verboseLogging) { - Log.d(TAG, "Springing ${getReadablePropertyName(property)} to $toPosition.") - } - - springConfigs[property] = - SpringConfig(stiffness, dampingRatio, startVelocity, toPosition) - return this - } - - /** - * Springs a property to a given value using the provided start velocity and configuration - * options. - * - * @see spring - */ - fun spring( - property: FloatPropertyCompat<in T>, - toPosition: Float, - startVelocity: Float, - config: SpringConfig = defaultSpring - ): PhysicsAnimator<T> { - return spring( - property, toPosition, startVelocity, config.stiffness, config.dampingRatio) - } - - /** - * Springs a property to a given value using the provided configuration options, and a start - * velocity of 0f. - * - * @see spring - */ - fun spring( - property: FloatPropertyCompat<in T>, - toPosition: Float, - config: SpringConfig = defaultSpring - ): PhysicsAnimator<T> { - return spring(property, toPosition, 0f, config) - } - - /** - * Springs a property to a given value using the provided configuration options, and a start - * velocity of 0f. - * - * @see spring - */ - fun spring( - property: FloatPropertyCompat<in T>, - toPosition: Float - ): PhysicsAnimator<T> { - return spring(property, toPosition, 0f) - } - - /** - * Flings a property using the given start velocity, using a [FlingAnimation] configured using - * the provided configuration settings. - * - * Flings are used when you have a start velocity, and want the property value to realistically - * decrease as friction is applied until the velocity reaches zero. Flings do not have a - * deterministic end value. If you are attempting to animate to a specific end value, use - * [spring]. - * - * If you find yourself repeating the same friction/min/max values, consider storing a single - * [FlingConfig] and passing that in instead. - * - * @param property The property to fling using the given start velocity. - * @param startVelocity The start velocity (in pixels per second) with which to start the fling. - * @param friction Friction value applied to slow down the animation over time. Higher values - * will more quickly slow the animation. Typical friction values range from 1f to 10f. - * @param min The minimum value allowed for the animation. If this value is reached, the - * animation will end abruptly. - * @param max The maximum value allowed for the animation. If this value is reached, the - * animation will end abruptly. - */ - fun fling( - property: FloatPropertyCompat<in T>, - startVelocity: Float, - friction: Float = defaultFling.friction, - min: Float = defaultFling.min, - max: Float = defaultFling.max - ): PhysicsAnimator<T> { - if (verboseLogging) { - Log.d(TAG, "Flinging ${getReadablePropertyName(property)} " + - "with velocity $startVelocity.") - } - - flingConfigs[property] = FlingConfig(friction, min, max, startVelocity) - return this - } - - /** - * Flings a property using the given start velocity, using a [FlingAnimation] configured using - * the provided configuration settings. - * - * @see fling - */ - fun fling( - property: FloatPropertyCompat<in T>, - startVelocity: Float, - config: FlingConfig = defaultFling - ): PhysicsAnimator<T> { - return fling(property, startVelocity, config.friction, config.min, config.max) - } - - /** - * Flings a property using the given start velocity. If the fling animation reaches the min/max - * bounds (from the [flingConfig]) with velocity remaining, it'll overshoot it and spring back. - * - * If the object is already out of the fling bounds, it will immediately spring back within - * bounds. - * - * This is useful for animating objects that are bounded by constraints such as screen edges, - * since otherwise the fling animation would end abruptly upon reaching the min/max bounds. - * - * @param property The property to animate. - * @param startVelocity The velocity, in pixels/second, with which to start the fling. If the - * object is already outside the fling bounds, this velocity will be used as the start velocity - * of the spring that will spring it back within bounds. - * @param flingMustReachMinOrMax If true, the fling animation is guaranteed to reach either its - * minimum bound (if [startVelocity] is negative) or maximum bound (if it's positive). The - * animator will use startVelocity if it's sufficient, or add more velocity if necessary. This - * is useful when fling's deceleration-based physics are preferable to the acceleration-based - * forces used by springs - typically, when you're allowing the user to move an object somewhere - * on the screen, but it needs to be along an edge. - * @param flingConfig The configuration to use for the fling portion of the animation. - * @param springConfig The configuration to use for the spring portion of the animation. - */ - @JvmOverloads - fun flingThenSpring( - property: FloatPropertyCompat<in T>, - startVelocity: Float, - flingConfig: FlingConfig, - springConfig: SpringConfig, - flingMustReachMinOrMax: Boolean = false - ): PhysicsAnimator<T> { - val target = weakTarget.get() - if (target == null) { - Log.w(TAG, "Trying to animate a GC-ed target.") - return this - } - val flingConfigCopy = flingConfig.copy() - val springConfigCopy = springConfig.copy() - val toAtLeast = if (startVelocity < 0) flingConfig.min else flingConfig.max - - if (flingMustReachMinOrMax && isValidValue(toAtLeast)) { - val currentValue = property.getValue(target) - val flingTravelDistance = - startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) - val projectedFlingEndValue = currentValue + flingTravelDistance - val midpoint = (flingConfig.min + flingConfig.max) / 2 - - // If fling velocity is too low to push the target past the midpoint between min and - // max, then spring back towards the nearest edge, starting with the current velocity. - if ((startVelocity < 0 && projectedFlingEndValue > midpoint) || - (startVelocity > 0 && projectedFlingEndValue < midpoint)) { - val toPosition = - if (projectedFlingEndValue < midpoint) flingConfig.min else flingConfig.max - if (isValidValue(toPosition)) { - return spring(property, toPosition, startVelocity, springConfig) - } - } - - // Projected fling end value is past the midpoint, so fling forward. - val distanceToDestination = toAtLeast - property.getValue(target) - - // The minimum velocity required for the fling to end up at the given destination, - // taking the provided fling friction value. - val velocityToReachDestination = distanceToDestination * - (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) - - // If there's distance to cover, and the provided velocity is moving in the correct - // direction, ensure that the velocity is high enough to reach the destination. - // Otherwise, just use startVelocity - this means that the fling is at or out of bounds. - // The fling will immediately end and a spring will bring the object back into bounds - // with this startVelocity. - flingConfigCopy.startVelocity = when { - distanceToDestination > 0f && startVelocity >= 0f -> - max(velocityToReachDestination, startVelocity) - distanceToDestination < 0f && startVelocity <= 0f -> - min(velocityToReachDestination, startVelocity) - else -> startVelocity - } - - springConfigCopy.finalPosition = toAtLeast - } else { - flingConfigCopy.startVelocity = startVelocity - } - - flingConfigs[property] = flingConfigCopy - springConfigs[property] = springConfigCopy - return this - } - - private fun isValidValue(value: Float) = value < Float.MAX_VALUE && value > -Float.MAX_VALUE - - /** - * Adds a listener that will be called whenever any property on the animated object is updated. - * This will be called on every animation frame, with the current value of the animated object - * and the new property values. - */ - fun addUpdateListener(listener: UpdateListener<T>): PhysicsAnimator<T> { - updateListeners.add(listener) - return this - } - - /** - * Adds a listener that will be called when a property stops animating. This is useful if - * you care about a specific property ending, or want to use the end value/end velocity from a - * particular property's animation. If you just want to run an action when all property - * animations have ended, use [withEndActions]. - */ - fun addEndListener(listener: EndListener<T>): PhysicsAnimator<T> { - endListeners.add(listener) - return this - } - - /** - * Adds end actions that will be run sequentially when animations for every property involved in - * this specific animation have ended (unless they were explicitly canceled). For example, if - * you call: - * - * animator - * .spring(TRANSLATION_X, ...) - * .spring(TRANSLATION_Y, ...) - * .withEndAction(action) - * .start() - * - * 'action' will be run when both TRANSLATION_X and TRANSLATION_Y end. - * - * Other properties may still be animating, if those animations were not started in the same - * call. For example: - * - * animator - * .spring(ALPHA, ...) - * .start() - * - * animator - * .spring(TRANSLATION_X, ...) - * .spring(TRANSLATION_Y, ...) - * .withEndAction(action) - * .start() - * - * 'action' will still be run as soon as TRANSLATION_X and TRANSLATION_Y end, even if ALPHA is - * still animating. - * - * If you want to run actions as soon as a subset of property animations have ended, you want - * access to the animation's end value/velocity, or you want to run these actions even if the - * animation is explicitly canceled, use [addEndListener]. End listeners have an allEnded param, - * which indicates that all relevant animations have ended. - */ - fun withEndActions(vararg endActions: EndAction?): PhysicsAnimator<T> { - this.endActions.addAll(endActions.filterNotNull()) - return this - } - - /** - * Helper overload so that callers from Java can use Runnables or method references as end - * actions without having to explicitly return Unit. - */ - fun withEndActions(vararg endActions: Runnable?): PhysicsAnimator<T> { - this.endActions.addAll(endActions.filterNotNull().map { it::run }) - return this - } - - fun setDefaultSpringConfig(defaultSpring: SpringConfig) { - this.defaultSpring = defaultSpring - } - - fun setDefaultFlingConfig(defaultFling: FlingConfig) { - this.defaultFling = defaultFling - } - - /** - * Set the custom AnimationHandler for all aniatmion in this animator. Set this with null for - * restoring to default AnimationHandler. - */ - fun setCustomAnimationHandler(handler: AnimationHandler) { - this.customAnimationHandler = handler - } - - /** Starts the animations! */ - fun start() { - startAction() - } - - /** - * Starts the animations for real! This is typically called immediately by [start] unless this - * animator is under test. - */ - internal fun startInternal() { - if (!Looper.getMainLooper().isCurrentThread) { - Log.e(TAG, "Animations can only be started on the main thread. If you are seeing " + - "this message in a test, call PhysicsAnimatorTestUtils#prepareForTest in " + - "your test setup.") - } - val target = weakTarget.get() - if (target == null) { - Log.w(TAG, "Trying to animate a GC-ed object.") - return - } - - // Functions that will actually start the animations. These are run after we build and add - // the InternalListener, since some animations might update/end immediately and we don't - // want to miss those updates. - val animationStartActions = ArrayList<() -> Unit>() - - for (animatedProperty in getAnimatedProperties()) { - val flingConfig = flingConfigs[animatedProperty] - val springConfig = springConfigs[animatedProperty] - - // The property's current value on the object. - val currentValue = animatedProperty.getValue(target) - - // Start by checking for a fling configuration. If one is present, we're either flinging - // or flinging-then-springing. Either way, we'll want to start the fling first. - if (flingConfig != null) { - animationStartActions.add { - // When the animation is starting, adjust the min/max bounds to include the - // current value of the property, if necessary. This is required to allow a - // fling to bring an out-of-bounds object back into bounds. For example, if an - // object was dragged halfway off the left side of the screen, but then flung to - // the right, we don't want the animation to end instantly just because the - // object started out of bounds. If the fling is in the direction that would - // take it farther out of bounds, it will end instantly as expected. - flingConfig.apply { - min = min(currentValue, this.min) - max = max(currentValue, this.max) - } - - // Flings can't be updated to a new position while maintaining velocity, because - // we're using the explicitly provided start velocity. Cancel any flings (or - // springs) on this property before flinging. - cancel(animatedProperty) - - // Apply the custom animation handler if it not null - val flingAnim = getFlingAnimation(animatedProperty, target) - flingAnim.animationHandler = - customAnimationHandler ?: flingAnim.animationHandler - - // Apply the configuration and start the animation. - flingAnim.also { flingConfig.applyToAnimation(it) }.start() - } - } - - // Check for a spring configuration. If one is present, we're either springing, or - // flinging-then-springing. - if (springConfig != null) { - - // If there is no corresponding fling config, we're only springing. - if (flingConfig == null) { - // Apply the configuration and start the animation. - val springAnim = getSpringAnimation(animatedProperty, target) - - // If customAnimationHander is exist and has not been set to the animation, - // it should set here. - if (customAnimationHandler != null && - springAnim.animationHandler != customAnimationHandler) { - // Cancel the animation before set animation handler - if (springAnim.isRunning) { - cancel(animatedProperty) - } - // Apply the custom animation handler if it not null - springAnim.animationHandler = - customAnimationHandler ?: springAnim.animationHandler - } - - // Apply the configuration and start the animation. - springConfig.applyToAnimation(springAnim) - animationStartActions.add(springAnim::start) - } else { - // If there's a corresponding fling config, we're flinging-then-springing. Save - // the fling's original bounds so we can spring to them when the fling ends. - val flingMin = flingConfig.min - val flingMax = flingConfig.max - - // Add an end listener that will start the spring when the fling ends. - endListeners.add(0, object : EndListener<T> { - override fun onAnimationEnd( - target: T, - property: FloatPropertyCompat<in T>, - wasFling: Boolean, - canceled: Boolean, - finalValue: Float, - finalVelocity: Float, - allRelevantPropertyAnimsEnded: Boolean - ) { - // If this isn't the relevant property, it wasn't a fling, or the fling - // was explicitly cancelled, don't spring. - if (property != animatedProperty || !wasFling || canceled) { - return - } - - val endedWithVelocity = abs(finalVelocity) > 0 - - // If the object was out of bounds when the fling animation started, it - // will immediately end. In that case, we'll spring it back in bounds. - val endedOutOfBounds = finalValue !in flingMin..flingMax - - // If the fling ended either out of bounds or with remaining velocity, - // it's time to spring. - if (endedWithVelocity || endedOutOfBounds) { - springConfig.startVelocity = finalVelocity - - // If the spring's final position isn't set, this is a - // flingThenSpring where flingMustReachMinOrMax was false. We'll - // need to set the spring's final position here. - if (springConfig.finalPosition == UNSET) { - if (endedWithVelocity) { - // If the fling ended with negative velocity, that means it - // hit the min bound, so spring to that bound (and vice - // versa). - springConfig.finalPosition = - if (finalVelocity < 0) flingMin else flingMax - } else if (endedOutOfBounds) { - // If the fling ended out of bounds, spring it to the - // nearest bound. - springConfig.finalPosition = - if (finalValue < flingMin) flingMin else flingMax - } - } - - // Apply the custom animation handler if it not null - val springAnim = getSpringAnimation(animatedProperty, target) - springAnim.animationHandler = - customAnimationHandler ?: springAnim.animationHandler - - // Apply the configuration and start the spring animation. - springAnim.also { springConfig.applyToAnimation(it) }.start() - } - } - }) - } - } - } - - // Add an internal listener that will dispatch animation events to the provided listeners. - internalListeners.add(InternalListener( - target, - getAnimatedProperties(), - ArrayList(updateListeners), - ArrayList(endListeners), - ArrayList(endActions))) - - // Actually start the DynamicAnimations. This is delayed until after the InternalListener is - // constructed and added so that we don't miss the end listener firing for any animations - // that immediately end. - animationStartActions.forEach { it.invoke() } - - clearAnimator() - } - - /** Clear the animator's builder variables. */ - private fun clearAnimator() { - springConfigs.clear() - flingConfigs.clear() - - updateListeners.clear() - endListeners.clear() - endActions.clear() - } - - /** Retrieves a spring animation for the given property, building one if needed. */ - private fun getSpringAnimation( - property: FloatPropertyCompat<in T>, - target: T - ): SpringAnimation { - return springAnimations.getOrPut( - property, - { configureDynamicAnimation(SpringAnimation(target, property), property) - as SpringAnimation }) - } - - /** Retrieves a fling animation for the given property, building one if needed. */ - private fun getFlingAnimation(property: FloatPropertyCompat<in T>, target: T): FlingAnimation { - return flingAnimations.getOrPut( - property, - { configureDynamicAnimation(FlingAnimation(target, property), property) - as FlingAnimation }) - } - - /** - * Adds update and end listeners to the DynamicAnimation which will dispatch to the internal - * listeners. - */ - private fun configureDynamicAnimation( - anim: DynamicAnimation<*>, - property: FloatPropertyCompat<in T> - ): DynamicAnimation<*> { - anim.addUpdateListener { _, value, velocity -> - for (i in 0 until internalListeners.size) { - internalListeners[i].onInternalAnimationUpdate(property, value, velocity) - } - } - anim.addEndListener { _, canceled, value, velocity -> - internalListeners.removeAll { - it.onInternalAnimationEnd( - property, canceled, value, velocity, anim is FlingAnimation) - } - if (springAnimations[property] == anim) { - springAnimations.remove(property) - } - if (flingAnimations[property] == anim) { - flingAnimations.remove(property) - } - } - return anim - } - - /** - * Internal listener class that receives updates from DynamicAnimation listeners, and dispatches - * them to the appropriate update/end listeners. This class is also aware of which properties - * were being animated when the end listeners were passed in, so that we can provide the - * appropriate value for allEnded to [EndListener.onAnimationEnd]. - */ - internal inner class InternalListener constructor( - private val target: T, - private var properties: Set<FloatPropertyCompat<in T>>, - private var updateListeners: List<UpdateListener<T>>, - private var endListeners: List<EndListener<T>>, - private var endActions: List<EndAction> - ) { - - /** The number of properties whose animations haven't ended. */ - private var numPropertiesAnimating = properties.size - - /** - * Update values that haven't yet been dispatched because not all property animations have - * updated yet. - */ - private val undispatchedUpdates = - ArrayMap<FloatPropertyCompat<in T>, AnimationUpdate>() - - /** Called when a DynamicAnimation updates. */ - internal fun onInternalAnimationUpdate( - property: FloatPropertyCompat<in T>, - value: Float, - velocity: Float - ) { - - // If this property animation isn't relevant to this listener, ignore it. - if (!properties.contains(property)) { - return - } - - undispatchedUpdates[property] = AnimationUpdate(value, velocity) - maybeDispatchUpdates() - } - - /** - * Called when a DynamicAnimation ends. - * - * @return True if this listener should be removed from the list of internal listeners, so - * it no longer receives updates from DynamicAnimations. - */ - internal fun onInternalAnimationEnd( - property: FloatPropertyCompat<in T>, - canceled: Boolean, - finalValue: Float, - finalVelocity: Float, - isFling: Boolean - ): Boolean { - - // If this property animation isn't relevant to this listener, ignore it. - if (!properties.contains(property)) { - return false - } - - // Dispatch updates if we have one for each property. - numPropertiesAnimating-- - maybeDispatchUpdates() - - // If we didn't have an update for each property, dispatch the update for the ending - // property. This guarantees that an update isn't sent for this property *after* we call - // onAnimationEnd for that property. - if (undispatchedUpdates.contains(property)) { - updateListeners.forEach { updateListener -> - updateListener.onAnimationUpdateForProperty( - target, - UpdateMap<T>().also { it[property] = undispatchedUpdates[property] }) - } - - undispatchedUpdates.remove(property) - } - - val allEnded = !arePropertiesAnimating(properties) - endListeners.forEach { - it.onAnimationEnd( - target, property, isFling, canceled, finalValue, finalVelocity, - allEnded) - - // Check that the end listener didn't restart this property's animation. - if (isPropertyAnimating(property)) { - return false - } - } - - // If all of the animations that this listener cares about have ended, run the end - // actions unless the animation was canceled. - if (allEnded && !canceled) { - endActions.forEach { it() } - } - - return allEnded - } - - /** - * Dispatch undispatched values if we've received an update from each of the animating - * properties. - */ - private fun maybeDispatchUpdates() { - if (undispatchedUpdates.size >= numPropertiesAnimating && - undispatchedUpdates.size > 0) { - updateListeners.forEach { - it.onAnimationUpdateForProperty(target, ArrayMap(undispatchedUpdates)) - } - - undispatchedUpdates.clear() - } - } - } - - /** Return true if any animations are running on the object. */ - fun isRunning(): Boolean { - return arePropertiesAnimating(springAnimations.keys.union(flingAnimations.keys)) - } - - /** Returns whether the given property is animating. */ - fun isPropertyAnimating(property: FloatPropertyCompat<in T>): Boolean { - return springAnimations[property]?.isRunning ?: false || - flingAnimations[property]?.isRunning ?: false - } - - /** Returns whether any of the given properties are animating. */ - fun arePropertiesAnimating(properties: Set<FloatPropertyCompat<in T>>): Boolean { - return properties.any { isPropertyAnimating(it) } - } - - /** Return the set of properties that will begin animating upon calling [start]. */ - internal fun getAnimatedProperties(): Set<FloatPropertyCompat<in T>> { - return springConfigs.keys.union(flingConfigs.keys) - } - - /** - * Cancels the given properties. This is typically called immediately by [cancel], unless this - * animator is under test. - */ - internal fun cancelInternal(properties: Set<FloatPropertyCompat<in T>>) { - for (property in properties) { - flingAnimations[property]?.cancel() - springAnimations[property]?.cancel() - } - } - - /** Cancels all in progress animations on all properties. */ - fun cancel() { - cancelAction(flingAnimations.keys) - cancelAction(springAnimations.keys) - } - - /** Cancels in progress animations on the provided properties only. */ - fun cancel(vararg properties: FloatPropertyCompat<in T>) { - cancelAction(properties.toSet()) - } - - /** - * Container object for spring animation configuration settings. This allows you to store - * default stiffness and damping ratio values in a single configuration object, which you can - * pass to [spring]. - */ - data class SpringConfig internal constructor( - internal var stiffness: Float, - internal var dampingRatio: Float, - internal var startVelocity: Float = 0f, - internal var finalPosition: Float = UNSET - ) { - - constructor() : - this(globalDefaultSpring.stiffness, globalDefaultSpring.dampingRatio) - - constructor(stiffness: Float, dampingRatio: Float) : - this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f) - - /** Apply these configuration settings to the given SpringAnimation. */ - internal fun applyToAnimation(anim: SpringAnimation) { - val springForce = anim.spring ?: SpringForce() - anim.spring = springForce.apply { - stiffness = this@SpringConfig.stiffness - dampingRatio = this@SpringConfig.dampingRatio - finalPosition = this@SpringConfig.finalPosition - } - - if (startVelocity != 0f) anim.setStartVelocity(startVelocity) - } - } - - /** - * Container object for fling animation configuration settings. This allows you to store default - * friction values (as well as optional min/max values) in a single configuration object, which - * you can pass to [fling] and related methods. - */ - data class FlingConfig internal constructor( - internal var friction: Float, - internal var min: Float, - internal var max: Float, - internal var startVelocity: Float - ) { - - constructor() : this(globalDefaultFling.friction) - - constructor(friction: Float) : - this(friction, globalDefaultFling.min, globalDefaultFling.max) - - constructor(friction: Float, min: Float, max: Float) : - this(friction, min, max, startVelocity = 0f) - - /** Apply these configuration settings to the given FlingAnimation. */ - internal fun applyToAnimation(anim: FlingAnimation) { - anim.apply { - friction = this@FlingConfig.friction - setMinValue(min) - setMaxValue(max) - setStartVelocity(startVelocity) - } - } - } - - /** - * Listener for receiving values from in progress animations. Used with - * [PhysicsAnimator.addUpdateListener]. - * - * @param <T> The type of the object being animated. - </T> */ - interface UpdateListener<T> { - - /** - * Called on each animation frame with the target object, and a map of FloatPropertyCompat - * -> AnimationUpdate, containing the latest value and velocity for that property. When - * multiple properties are animating together, the map will typically contain one entry for - * each property. However, you should never assume that this is the case - when a property - * animation ends earlier than the others, you'll receive an UpdateMap containing only that - * property's final update. Subsequently, you'll only receive updates for the properties - * that are still animating. - * - * Always check that the map contains an update for the property you're interested in before - * accessing it. - * - * @param target The animated object itself. - * @param values Map of property to AnimationUpdate, which contains that property - * animation's latest value and velocity. You should never assume that a particular property - * is present in this map. - */ - fun onAnimationUpdateForProperty( - target: T, - values: UpdateMap<T> - ) - } - - /** - * Listener for receiving callbacks when animations end. - * - * @param <T> The type of the object being animated. - </T> */ - interface EndListener<T> { - - /** - * Called with the final animation values as each property animation ends. This can be used - * to respond to specific property animations concluding (such as hiding a view when ALPHA - * ends, even if the corresponding TRANSLATION animations have not ended). - * - * If you just want to run an action when all of the property animations have ended, you can - * use [PhysicsAnimator.withEndActions]. - * - * @param target The animated object itself. - * @param property The property whose animation has just ended. - * @param wasFling Whether this property ended after a fling animation (as opposed to a - * spring animation). If this property was animated via [flingThenSpring], this will be true - * if the fling animation did not reach the min/max bounds, decelerating to a stop - * naturally. It will be false if it hit the bounds and was sprung back. - * @param canceled Whether the animation was explicitly canceled before it naturally ended. - * @param finalValue The final value of the animated property. - * @param finalVelocity The final velocity (in pixels per second) of the ended animation. - * This is typically zero, unless this was a fling animation which ended abruptly due to - * reaching its configured min/max values. - * @param allRelevantPropertyAnimsEnded Whether all properties relevant to this end listener - * have ended. Relevant properties are those which were animated alongside the - * [addEndListener] call where this animator was passed in. For example: - * - * animator - * .spring(TRANSLATION_X, 100f) - * .spring(TRANSLATION_Y, 200f) - * .withEndListener(firstEndListener) - * .start() - * - * firstEndListener will be called first for TRANSLATION_X, with allEnded = false, - * because TRANSLATION_Y is still running. When TRANSLATION_Y ends, it'll be called with - * allEnded = true. - * - * If a subsequent call to start() is made with other properties, those properties are not - * considered relevant and allEnded will still equal true when only TRANSLATION_X and - * TRANSLATION_Y end. For example, if immediately after the prior example, while - * TRANSLATION_X and TRANSLATION_Y are still animating, we called: - * - * animator. - * .spring(SCALE_X, 2f, stiffness = 10f) // That will take awhile... - * .withEndListener(secondEndListener) - * .start() - * - * firstEndListener will still be called with allEnded = true when TRANSLATION_X/Y end, even - * though SCALE_X is still animating. Similarly, secondEndListener will be called with - * allEnded = true as soon as SCALE_X ends, even if the translation animations are still - * running. - */ - fun onAnimationEnd( - target: T, - property: FloatPropertyCompat<in T>, - wasFling: Boolean, - canceled: Boolean, - finalValue: Float, - finalVelocity: Float, - allRelevantPropertyAnimsEnded: Boolean - ) - } - - companion object { - - /** - * Constructor to use to for new physics animator instances in [getInstance]. This is - * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that - * all code using the physics animator is given testable instances instead. - */ - internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator - - @JvmStatic - @Suppress("UNCHECKED_CAST") - fun <T : Any> getInstance(target: T): PhysicsAnimator<T> { - if (!animators.containsKey(target)) { - animators[target] = instanceConstructor(target) - } - - return animators[target] as PhysicsAnimator<T> - } - - /** - * Set whether all physics animators should log a lot of information about animations. - * Useful for debugging! - */ - @JvmStatic - fun setVerboseLogging(debug: Boolean) { - verboseLogging = debug - } - - /** - * Estimates the end value of a fling that starts at the given value using the provided - * start velocity and fling configuration. - * - * This is only an estimate. Fling animations use a timing-based physics simulation that is - * non-deterministic, so this exact value may not be reached. - */ - @JvmStatic - fun estimateFlingEndValue( - startValue: Float, - startVelocity: Float, - flingConfig: FlingConfig - ): Float { - val distance = startVelocity / (flingConfig.friction * FLING_FRICTION_SCALAR_MULTIPLIER) - return Math.min(flingConfig.max, Math.max(flingConfig.min, startValue + distance)) - } - - @JvmStatic - fun getReadablePropertyName(property: FloatPropertyCompat<*>): String { - return when (property) { - DynamicAnimation.TRANSLATION_X -> "translationX" - DynamicAnimation.TRANSLATION_Y -> "translationY" - DynamicAnimation.TRANSLATION_Z -> "translationZ" - DynamicAnimation.SCALE_X -> "scaleX" - DynamicAnimation.SCALE_Y -> "scaleY" - DynamicAnimation.ROTATION -> "rotation" - DynamicAnimation.ROTATION_X -> "rotationX" - DynamicAnimation.ROTATION_Y -> "rotationY" - DynamicAnimation.SCROLL_X -> "scrollX" - DynamicAnimation.SCROLL_Y -> "scrollY" - DynamicAnimation.ALPHA -> "alpha" - else -> "Custom FloatPropertyCompat instance" - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt deleted file mode 100644 index c50eeac80d7a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimatorTestUtils.kt +++ /dev/null @@ -1,473 +0,0 @@ -/* - * Copyright (C) 2019 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.util.animation - -import android.os.Handler -import android.os.Looper -import android.util.ArrayMap -import androidx.dynamicanimation.animation.FloatPropertyCompat -import com.android.systemui.util.animation.PhysicsAnimatorTestUtils.prepareForTest -import java.util.ArrayDeque -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -typealias UpdateMatcher = (PhysicsAnimator.AnimationUpdate) -> Boolean -typealias UpdateFramesPerProperty<T> = - ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>> - -/** - * Utilities for testing code that uses [PhysicsAnimator]. - * - * Start by calling [prepareForTest] at the beginning of each test - this will modify the behavior - * of all PhysicsAnimator instances so that they post animations to the main thread (so they don't - * crash). It'll also enable the use of the other static helper methods in this class, which you can - * use to do things like block the test until animations complete (so you can test end states), or - * verify keyframes. - */ -object PhysicsAnimatorTestUtils { - var timeoutMs: Long = 2000 - private var startBlocksUntilAnimationsEnd = false - private val animationThreadHandler = Handler(Looper.getMainLooper()) - private val allAnimatedObjects = HashSet<Any>() - private val animatorTestHelpers = HashMap<PhysicsAnimator<*>, AnimatorTestHelper<*>>() - - /** - * Modifies the behavior of all [PhysicsAnimator] instances so that they post animations to the - * main thread, and report all of their - */ - @JvmStatic - fun prepareForTest() { - val defaultConstructor = PhysicsAnimator.instanceConstructor - PhysicsAnimator.instanceConstructor = fun(target: Any): PhysicsAnimator<*> { - val animator = defaultConstructor(target) - allAnimatedObjects.add(target) - animatorTestHelpers[animator] = AnimatorTestHelper(animator) - return animator - } - - timeoutMs = 2000 - startBlocksUntilAnimationsEnd = false - allAnimatedObjects.clear() - } - - @JvmStatic - fun tearDown() { - val latch = CountDownLatch(1) - animationThreadHandler.post { - animatorTestHelpers.keys.forEach { it.cancel() } - latch.countDown() - } - - latch.await() - - animatorTestHelpers.clear() - animators.clear() - allAnimatedObjects.clear() - } - - /** - * Sets the maximum time (in milliseconds) to block the test thread while waiting for animations - * before throwing an exception. - */ - @JvmStatic - fun setBlockTimeout(timeoutMs: Long) { - this.timeoutMs = timeoutMs - } - - /** - * Sets whether all animations should block the test thread until they end. This is typically - * the desired behavior, since you can invoke code that runs an animation and then assert things - * about its end state. - */ - @JvmStatic - fun setAllAnimationsBlock(block: Boolean) { - startBlocksUntilAnimationsEnd = block - } - - /** - * Blocks the calling thread until animations of the given property on the target object end. - */ - @JvmStatic - @Throws(InterruptedException::class) - fun <T : Any> blockUntilAnimationsEnd( - animator: PhysicsAnimator<T>, - vararg properties: FloatPropertyCompat<in T> - ) { - val animatingProperties = HashSet<FloatPropertyCompat<in T>>() - for (property in properties) { - if (animator.isPropertyAnimating(property)) { - animatingProperties.add(property) - } - } - - if (animatingProperties.size > 0) { - val latch = CountDownLatch(animatingProperties.size) - getAnimationTestHelper(animator).addTestEndListener( - object : PhysicsAnimator.EndListener<T> { - override fun onAnimationEnd( - target: T, - property: FloatPropertyCompat<in T>, - wasFling: Boolean, - canceled: Boolean, - finalValue: Float, - finalVelocity: Float, - allRelevantPropertyAnimsEnded: Boolean - ) { - if (animatingProperties.contains(property)) { - latch.countDown() - } - } - }) - - latch.await(timeoutMs, TimeUnit.MILLISECONDS) - } - } - - /** - * Blocks the calling thread until all animations of the given property (on all target objects) - * have ended. Useful when you don't have access to the objects being animated, but still need - * to wait for them to end so that other testable side effects occur (such as update/end - * listeners). - */ - @JvmStatic - @Throws(InterruptedException::class) - @Suppress("UNCHECKED_CAST") - fun <T : Any> blockUntilAnimationsEnd( - properties: FloatPropertyCompat<in T> - ) { - for (target in allAnimatedObjects) { - try { - blockUntilAnimationsEnd( - PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, properties) - } catch (e: ClassCastException) { - // Keep checking the other objects for ones whose types match the provided - // properties. - } - } - } - - /** - * Blocks the calling thread until the first animation frame in which predicate returns true. If - * the given object isn't animating, returns without blocking. - */ - @JvmStatic - @Throws(InterruptedException::class) - fun <T : Any> blockUntilFirstAnimationFrameWhereTrue( - animator: PhysicsAnimator<T>, - predicate: (T) -> Boolean - ) { - if (animator.isRunning()) { - val latch = CountDownLatch(1) - getAnimationTestHelper(animator).addTestUpdateListener(object : PhysicsAnimator - .UpdateListener<T> { - override fun onAnimationUpdateForProperty( - target: T, - values: UpdateMap<T> - ) { - if (predicate(target)) { - latch.countDown() - } - } - }) - - latch.await(timeoutMs, TimeUnit.MILLISECONDS) - } - } - - /** - * Verifies that the animator reported animation frame values to update listeners that satisfy - * the given matchers, in order. Not all frames need to satisfy a matcher - we'll run through - * all animation frames, and check them against the current predicate. If it returns false, we - * continue through the frames until it returns true, and then move on to the next matcher. - * Verification fails if we run out of frames while unsatisfied matchers remain. - * - * If verification is successful, all frames to this point are considered 'verified' and will be - * cleared. Subsequent calls to this method will start verification at the next animation frame. - * - * Example: Verify that an animation surpassed x = 50f before going negative. - * verifyAnimationUpdateFrames( - * animator, TRANSLATION_X, - * { u -> u.value > 50f }, - * { u -> u.value < 0f }) - * - * Example: verify that an animation went backwards at some point while still being on-screen. - * verifyAnimationUpdateFrames( - * animator, TRANSLATION_X, - * { u -> u.velocity < 0f && u.value >= 0f }) - * - * This method is intended to help you test longer, more complicated animations where it's - * critical that certain values were reached. Using this method to test short animations can - * fail due to the animation having fewer frames than provided matchers. For example, an - * animation from x = 1f to x = 5f might only have two frames, at x = 3f and x = 5f. The - * following would then fail despite it seeming logically sound: - * - * verifyAnimationUpdateFrames( - * animator, TRANSLATION_X, - * { u -> u.value > 1f }, - * { u -> u.value > 2f }, - * { u -> u.value > 3f }) - * - * Tests might also fail if your matchers are too granular, such as this example test after an - * animation from x = 0f to x = 100f. It's unlikely there was a frame specifically between 2f - * and 3f. - * - * verifyAnimationUpdateFrames( - * animator, TRANSLATION_X, - * { u -> u.value > 2f && u.value < 3f }, - * { u -> u.value >= 50f }) - * - * Failures will print a helpful log of all animation frames so you can see what caused the test - * to fail. - */ - fun <T : Any> verifyAnimationUpdateFrames( - animator: PhysicsAnimator<T>, - property: FloatPropertyCompat<in T>, - firstUpdateMatcher: UpdateMatcher, - vararg additionalUpdateMatchers: UpdateMatcher - ) { - val updateFrames: UpdateFramesPerProperty<T> = getAnimationUpdateFrames(animator) - - if (!updateFrames.containsKey(property)) { - error("No frames for given target object and property.") - } - - // Copy the frames to avoid a ConcurrentModificationException if the animation update - // listeners attempt to add a new frame while we're verifying these. - val framesForProperty = ArrayList(updateFrames[property]!!) - val matchers = ArrayDeque<UpdateMatcher>( - additionalUpdateMatchers.toList()) - val frameTraceMessage = StringBuilder() - - var curMatcher = firstUpdateMatcher - - // Loop through the updates from the testable animator. - for (update in framesForProperty) { - - // Check whether this frame satisfies the current matcher. - if (curMatcher(update)) { - - // If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining - // frames and return without failing. - if (matchers.size == 0) { - getAnimationUpdateFrames(animator).remove(property) - return - } - - frameTraceMessage.append("$update\t(satisfied matcher)\n") - curMatcher = matchers.pop() // Get the next matcher and keep going. - } else { - frameTraceMessage.append("${update}\n") - } - } - - val readablePropertyName = PhysicsAnimator.getReadablePropertyName(property) - getAnimationUpdateFrames(animator).remove(property) - - throw RuntimeException( - "Failed to verify animation frames for property $readablePropertyName: " + - "Provided ${additionalUpdateMatchers.size + 1} matchers, " + - "however ${matchers.size + 1} remained unsatisfied.\n\n" + - "All frames:\n$frameTraceMessage") - } - - /** - * Overload of [verifyAnimationUpdateFrames] that builds matchers for you, from given float - * values. For example, to verify that an animations passed from 0f to 50f to 100f back to 50f: - * - * verifyAnimationUpdateFrames(animator, TRANSLATION_X, 0f, 50f, 100f, 50f) - * - * This verifies that update frames were received with values of >= 0f, >= 50f, >= 100f, and - * <= 50f. - * - * The same caveats apply: short animations might not have enough frames to satisfy all of the - * matchers, and overly specific calls (such as 0f, 1f, 2f, 3f, etc. for an animation from - * x = 0f to x = 100f) might fail as the animation only had frames at 0f, 25f, 50f, 75f, and - * 100f. As with [verifyAnimationUpdateFrames], failures will print a helpful log of all frames - * so you can see what caused the test to fail. - */ - fun <T : Any> verifyAnimationUpdateFrames( - animator: PhysicsAnimator<T>, - property: FloatPropertyCompat<in T>, - startValue: Float, - firstTargetValue: Float, - vararg additionalTargetValues: Float - ) { - val matchers = ArrayList<UpdateMatcher>() - - val values = ArrayList<Float>().also { - it.add(firstTargetValue) - it.addAll(additionalTargetValues.toList()) - } - - var prevVal = startValue - for (value in values) { - if (value > prevVal) { - matchers.add { update -> update.value >= value } - } else { - matchers.add { update -> update.value <= value } - } - - prevVal = value - } - - verifyAnimationUpdateFrames( - animator, property, matchers[0], *matchers.drop(0).toTypedArray()) - } - - /** - * Returns all of the values that have ever been reported to update listeners, per property. - */ - @Suppress("UNCHECKED_CAST") - fun <T : Any> getAnimationUpdateFrames(animator: PhysicsAnimator<T>): - UpdateFramesPerProperty<T> { - return animatorTestHelpers[animator]?.getUpdates() as UpdateFramesPerProperty<T> - } - - /** - * Clears animation frame updates from the given animator so they aren't used the next time its - * passed to [verifyAnimationUpdateFrames]. - */ - fun <T : Any> clearAnimationUpdateFrames(animator: PhysicsAnimator<T>) { - animatorTestHelpers[animator]?.clearUpdates() - } - - @Suppress("UNCHECKED_CAST") - private fun <T> getAnimationTestHelper(animator: PhysicsAnimator<T>): AnimatorTestHelper<T> { - return animatorTestHelpers[animator] as AnimatorTestHelper<T> - } - - /** - * Helper class for testing an animator. This replaces the animator's start action with - * [startForTest] and adds test listeners to enable other test utility behaviors. We build one - * these for each Animator and keep them around so we can access the updates. - */ - class AnimatorTestHelper<T> (private val animator: PhysicsAnimator<T>) { - - /** All updates received for each property animation. */ - private val allUpdates = - ArrayMap<FloatPropertyCompat<in T>, ArrayList<PhysicsAnimator.AnimationUpdate>>() - - private val testEndListeners = ArrayList<PhysicsAnimator.EndListener<T>>() - private val testUpdateListeners = ArrayList<PhysicsAnimator.UpdateListener<T>>() - - /** Whether we're currently in the middle of executing startInternal(). */ - private var currentlyRunningStartInternal = false - - init { - animator.startAction = ::startForTest - animator.cancelAction = ::cancelForTest - } - - internal fun addTestEndListener(listener: PhysicsAnimator.EndListener<T>) { - testEndListeners.add(listener) - } - - internal fun addTestUpdateListener(listener: PhysicsAnimator.UpdateListener<T>) { - testUpdateListeners.add(listener) - } - - internal fun getUpdates(): UpdateFramesPerProperty<T> { - return allUpdates - } - - internal fun clearUpdates() { - allUpdates.clear() - } - - private fun startForTest() { - // The testable animator needs to block the main thread until super.start() has been - // called, since callers expect .start() to be synchronous but we're posting it to a - // handler here. We may also continue blocking until all animations end, if - // startBlocksUntilAnimationsEnd = true. - val unblockLatch = CountDownLatch(if (startBlocksUntilAnimationsEnd) 2 else 1) - - animationThreadHandler.post { - // Add an update listener that dispatches to any test update listeners added by - // tests. - animator.addUpdateListener(object : PhysicsAnimator.UpdateListener<T> { - override fun onAnimationUpdateForProperty( - target: T, - values: ArrayMap<FloatPropertyCompat<in T>, PhysicsAnimator.AnimationUpdate> - ) { - values.forEach { (property, value) -> - allUpdates.getOrPut(property, { ArrayList() }).add(value) - } - - for (listener in testUpdateListeners) { - listener.onAnimationUpdateForProperty(target, values) - } - } - }) - - // Add an end listener that dispatches to any test end listeners added by tests, and - // unblocks the main thread if required. - animator.addEndListener(object : PhysicsAnimator.EndListener<T> { - override fun onAnimationEnd( - target: T, - property: FloatPropertyCompat<in T>, - wasFling: Boolean, - canceled: Boolean, - finalValue: Float, - finalVelocity: Float, - allRelevantPropertyAnimsEnded: Boolean - ) { - for (listener in testEndListeners) { - listener.onAnimationEnd( - target, property, wasFling, canceled, finalValue, finalVelocity, - allRelevantPropertyAnimsEnded) - } - - if (allRelevantPropertyAnimsEnded) { - testEndListeners.clear() - testUpdateListeners.clear() - - if (startBlocksUntilAnimationsEnd) { - unblockLatch.countDown() - } - } - } - }) - - currentlyRunningStartInternal = true - animator.startInternal() - currentlyRunningStartInternal = false - unblockLatch.countDown() - } - - unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS) - } - - private fun cancelForTest(properties: Set<FloatPropertyCompat<in T>>) { - // If this was called from startInternal, we are already on the animation thread, and - // should just call cancelInternal rather than posting it. If we post it, the - // cancellation will occur after the rest of startInternal() and we'll immediately - // cancel the animation we worked so hard to start! - if (currentlyRunningStartInternal) { - animator.cancelInternal(properties) - return - } - - val unblockLatch = CountDownLatch(1) - - animationThreadHandler.post { - animator.cancelInternal(properties) - unblockLatch.countDown() - } - - unblockLatch.await(timeoutMs, TimeUnit.MILLISECONDS) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt index 3347cf6ca2a4..603d423143ce 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt @@ -46,6 +46,7 @@ class TransitionLayout @JvmOverloads constructor( private var measureAsConstraint: Boolean = false private var currentState: TransitionViewState = TransitionViewState() private var updateScheduled = false + private var isPreDrawApplicatorRegistered = false private var desiredMeasureWidth = 0 private var desiredMeasureHeight = 0 @@ -74,6 +75,7 @@ class TransitionLayout @JvmOverloads constructor( override fun onPreDraw(): Boolean { updateScheduled = false viewTreeObserver.removeOnPreDrawListener(this) + isPreDrawApplicatorRegistered = false applyCurrentState() return true } @@ -94,6 +96,14 @@ class TransitionLayout @JvmOverloads constructor( } } + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + if (isPreDrawApplicatorRegistered) { + viewTreeObserver.removeOnPreDrawListener(preDrawApplicator) + isPreDrawApplicatorRegistered = false + } + } + /** * Apply the current state to the view and its widgets */ @@ -158,7 +168,10 @@ class TransitionLayout @JvmOverloads constructor( private fun applyCurrentStateOnPredraw() { if (!updateScheduled) { updateScheduled = true - viewTreeObserver.addOnPreDrawListener(preDrawApplicator) + if (!isPreDrawApplicatorRegistered) { + viewTreeObserver.addOnPreDrawListener(preDrawApplicator) + isPreDrawApplicatorRegistered = true + } } } diff --git a/packages/SystemUI/src/com/android/systemui/util/magnetictarget/MagnetizedObject.kt b/packages/SystemUI/src/com/android/systemui/util/magnetictarget/MagnetizedObject.kt deleted file mode 100644 index f441049feefb..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/magnetictarget/MagnetizedObject.kt +++ /dev/null @@ -1,699 +0,0 @@ -/* - * Copyright (C) 2020 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.util.magnetictarget - -import android.annotation.SuppressLint -import android.content.Context -import android.database.ContentObserver -import android.graphics.PointF -import android.os.Handler -import android.os.UserHandle -import android.os.VibrationEffect -import android.os.Vibrator -import android.provider.Settings -import android.view.MotionEvent -import android.view.VelocityTracker -import android.view.View -import android.view.ViewConfiguration -import androidx.dynamicanimation.animation.DynamicAnimation -import androidx.dynamicanimation.animation.FloatPropertyCompat -import androidx.dynamicanimation.animation.SpringForce -import com.android.systemui.util.animation.PhysicsAnimator -import kotlin.math.abs -import kotlin.math.hypot - -/** - * Utility class for creating 'magnetized' objects that are attracted to one or more magnetic - * targets. Magnetic targets attract objects that are dragged near them, and hold them there unless - * they're moved away or released. Releasing objects inside a magnetic target typically performs an - * action on the object. - * - * MagnetizedObject also supports flinging to targets, which will result in the object being pulled - * into the target and released as if it was dragged into it. - * - * To use this class, either construct an instance with an object of arbitrary type, or use the - * [MagnetizedObject.magnetizeView] shortcut method if you're magnetizing a view. Then, set - * [magnetListener] to receive event callbacks. In your touch handler, pass all MotionEvents - * that move this object to [maybeConsumeMotionEvent]. If that method returns true, consider the - * event consumed by the MagnetizedObject and don't move the object unless it begins returning false - * again. - * - * @param context Context, used to retrieve a Vibrator instance for vibration effects. - * @param underlyingObject The actual object that we're magnetizing. - * @param xProperty Property that sets the x value of the object's position. - * @param yProperty Property that sets the y value of the object's position. - */ -abstract class MagnetizedObject<T : Any>( - val context: Context, - - /** The actual object that is animated. */ - val underlyingObject: T, - - /** Property that gets/sets the object's X value. */ - val xProperty: FloatPropertyCompat<in T>, - - /** Property that gets/sets the object's Y value. */ - val yProperty: FloatPropertyCompat<in T> -) { - - /** Return the width of the object. */ - abstract fun getWidth(underlyingObject: T): Float - - /** Return the height of the object. */ - abstract fun getHeight(underlyingObject: T): Float - - /** - * Fill the provided array with the location of the top-left of the object, relative to the - * entire screen. Compare to [View.getLocationOnScreen]. - */ - abstract fun getLocationOnScreen(underlyingObject: T, loc: IntArray) - - /** Methods for listening to events involving a magnetized object. */ - interface MagnetListener { - - /** - * Called when touch events move within the magnetic field of a target, causing the - * object to animate to the target and become 'stuck' there. The animation happens - * automatically here - you should not move the object. You can, however, change its state - * to indicate to the user that it's inside the target and releasing it will have an effect. - * - * [maybeConsumeMotionEvent] is now returning true and will continue to do so until a call - * to [onUnstuckFromTarget] or [onReleasedInTarget]. - * - * @param target The target that the object is now stuck to. - */ - fun onStuckToTarget(target: MagneticTarget) - - /** - * Called when the object is no longer stuck to a target. This means that either touch - * events moved outside of the magnetic field radius, or that a forceful fling out of the - * target was detected. - * - * The object won't be automatically animated out of the target, since you're responsible - * for moving the object again. You should move it (or animate it) using your own - * movement/animation logic. - * - * Reverse any effects applied in [onStuckToTarget] here. - * - * If [wasFlungOut] is true, [maybeConsumeMotionEvent] returned true for the ACTION_UP event - * that concluded the fling. If [wasFlungOut] is false, that means a drag gesture is ongoing - * and [maybeConsumeMotionEvent] is now returning false. - * - * @param target The target that this object was just unstuck from. - * @param velX The X velocity of the touch gesture when it exited the magnetic field. - * @param velY The Y velocity of the touch gesture when it exited the magnetic field. - * @param wasFlungOut Whether the object was unstuck via a fling gesture. This means that - * an ACTION_UP event was received, and that the gesture velocity was sufficient to conclude - * that the user wants to un-stick the object despite no touch events occurring outside of - * the magnetic field radius. - */ - fun onUnstuckFromTarget( - target: MagneticTarget, - velX: Float, - velY: Float, - wasFlungOut: Boolean - ) - - /** - * Called when the object is released inside a target, or flung towards it with enough - * velocity to reach it. - * - * @param target The target that the object was released in. - */ - fun onReleasedInTarget(target: MagneticTarget) - } - - private val animator: PhysicsAnimator<T> = PhysicsAnimator.getInstance(underlyingObject) - private val objectLocationOnScreen = IntArray(2) - - /** - * Targets that have been added to this object. These will all be considered when determining - * magnetic fields and fling trajectories. - */ - private val associatedTargets = ArrayList<MagneticTarget>() - - private val velocityTracker: VelocityTracker = VelocityTracker.obtain() - private val vibrator: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator - - private var touchDown = PointF() - private var touchSlop = 0 - private var movedBeyondSlop = false - - /** Whether touch events are presently occurring within the magnetic field area of a target. */ - val objectStuckToTarget: Boolean - get() = targetObjectIsStuckTo != null - - /** The target the object is stuck to, or null if the object is not stuck to any target. */ - private var targetObjectIsStuckTo: MagneticTarget? = null - - /** - * Sets the listener to receive events. This must be set, or [maybeConsumeMotionEvent] - * will always return false and no magnetic effects will occur. - */ - lateinit var magnetListener: MagnetizedObject.MagnetListener - - /** - * Optional update listener to provide to the PhysicsAnimator that is used to spring the object - * into the target. - */ - var physicsAnimatorUpdateListener: PhysicsAnimator.UpdateListener<T>? = null - - /** - * Optional end listener to provide to the PhysicsAnimator that is used to spring the object - * into the target. - */ - var physicsAnimatorEndListener: PhysicsAnimator.EndListener<T>? = null - - /** - * Method that is called when the object should be animated stuck to the target. The default - * implementation uses the object's x and y properties to animate the object centered inside the - * target. You can override this if you need custom animation. - * - * The method is invoked with the MagneticTarget that the object is sticking to, the X and Y - * velocities of the gesture that brought the object into the magnetic radius, whether or not it - * was flung, and a callback you must call after your animation completes. - */ - var animateStuckToTarget: (MagneticTarget, Float, Float, Boolean, (() -> Unit)?) -> Unit = - ::animateStuckToTargetInternal - - /** - * Sets whether forcefully flinging the object vertically towards a target causes it to be - * attracted to the target and then released immediately, despite never being dragged within the - * magnetic field. - */ - var flingToTargetEnabled = true - - /** - * If fling to target is enabled, forcefully flinging the object towards a target will cause - * it to be attracted to the target and then released immediately, despite never being dragged - * within the magnetic field. - * - * This sets the width of the area considered 'near' enough a target to be considered a fling, - * in terms of percent of the target view's width. For example, setting this to 3f means that - * flings towards a 100px-wide target will be considered 'near' enough if they're towards the - * 300px-wide area around the target. - * - * Flings whose trajectory intersects the area will be attracted and released - even if the - * target view itself isn't intersected: - * - * | | - * | 0 | - * | / | - * | / | - * | X / | - * |.....###.....| - * - * - * Flings towards the target whose trajectories do not intersect the area will be treated as - * normal flings and the magnet will leave the object alone: - * - * | | - * | | - * | 0 | - * | / | - * | / X | - * |.....###.....| - * - */ - var flingToTargetWidthPercent = 3f - - /** - * Sets the minimum velocity (in pixels per second) required to fling an object to the target - * without dragging it into the magnetic field. - */ - var flingToTargetMinVelocity = 4000f - - /** - * Sets the minimum velocity (in pixels per second) required to fling un-stuck an object stuck - * to the target. If this velocity is reached, the object will be freed even if it wasn't moved - * outside the magnetic field radius. - */ - var flingUnstuckFromTargetMinVelocity = 4000f - - /** - * Sets the maximum X velocity above which the object will not stick to the target. Even if the - * object is dragged through the magnetic field, it will not stick to the target until the - * horizontal velocity is below this value. - */ - var stickToTargetMaxXVelocity = 2000f - - /** - * Enable or disable haptic vibration effects when the object interacts with the magnetic field. - * - * If you're experiencing crashes when the object enters targets, ensure that you have the - * android.permission.VIBRATE permission! - */ - var hapticsEnabled = true - - /** Default spring configuration to use for animating the object into a target. */ - var springConfig = PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_NO_BOUNCY) - - /** - * Spring configuration to use to spring the object into a target specifically when it's flung - * towards (rather than dragged near) it. - */ - var flungIntoTargetSpringConfig = springConfig - - init { - initHapticSettingObserver(context) - } - - /** - * Adds the provided MagneticTarget to this object. The object will now be attracted to the - * target if it strays within its magnetic field or is flung towards it. - * - * If this target (or its magnetic field) overlaps another target added to this object, the - * prior target will take priority. - */ - fun addTarget(target: MagneticTarget) { - associatedTargets.add(target) - target.updateLocationOnScreen() - } - - /** - * Shortcut that accepts a View and a magnetic field radius and adds it as a magnetic target. - * - * @return The MagneticTarget instance for the given View. This can be used to change the - * target's magnetic field radius after it's been added. It can also be added to other - * magnetized objects. - */ - fun addTarget(target: View, magneticFieldRadiusPx: Int): MagneticTarget { - return MagneticTarget(target, magneticFieldRadiusPx).also { addTarget(it) } - } - - /** - * Removes the given target from this object. The target will no longer attract the object. - */ - fun removeTarget(target: MagneticTarget) { - associatedTargets.remove(target) - } - - /** - * Provide this method with all motion events that move the magnetized object. If the - * location of the motion events moves within the magnetic field of a target, or indicate a - * fling-to-target gesture, this method will return true and you should not move the object - * yourself until it returns false again. - * - * Note that even when this method returns true, you should continue to pass along new motion - * events so that we know when the events move back outside the magnetic field area. - * - * This method will always return false if you haven't set a [magnetListener]. - */ - fun maybeConsumeMotionEvent(ev: MotionEvent): Boolean { - // Short-circuit if we don't have a listener or any targets, since those are required. - if (associatedTargets.size == 0) { - return false - } - - // When a gesture begins, recalculate target views' positions on the screen in case they - // have changed. Also, clear state. - if (ev.action == MotionEvent.ACTION_DOWN) { - updateTargetViews() - - // Clear the velocity tracker and stuck target. - velocityTracker.clear() - targetObjectIsStuckTo = null - - // Set the touch down coordinates and reset movedBeyondSlop. - touchDown.set(ev.rawX, ev.rawY) - movedBeyondSlop = false - } - - // Always pass events to the VelocityTracker. - addMovement(ev) - - // If we haven't yet moved beyond the slop distance, check if we have. - if (!movedBeyondSlop) { - val dragDistance = hypot(ev.rawX - touchDown.x, ev.rawY - touchDown.y) - if (dragDistance > touchSlop) { - // If we're beyond the slop distance, save that and continue. - movedBeyondSlop = true - } else { - // Otherwise, don't do anything yet. - return false - } - } - - val targetObjectIsInMagneticFieldOf = associatedTargets.firstOrNull { target -> - val distanceFromTargetCenter = hypot( - ev.rawX - target.centerOnScreen.x, - ev.rawY - target.centerOnScreen.y) - distanceFromTargetCenter < target.magneticFieldRadiusPx - } - - // If we aren't currently stuck to a target, and we're in the magnetic field of a target, - // we're newly stuck. - val objectNewlyStuckToTarget = - !objectStuckToTarget && targetObjectIsInMagneticFieldOf != null - - // If we are currently stuck to a target, we're in the magnetic field of a target, and that - // target isn't the one we're currently stuck to, then touch events have moved into a - // adjacent target's magnetic field. - val objectMovedIntoDifferentTarget = - objectStuckToTarget && - targetObjectIsInMagneticFieldOf != null && - targetObjectIsStuckTo != targetObjectIsInMagneticFieldOf - - if (objectNewlyStuckToTarget || objectMovedIntoDifferentTarget) { - velocityTracker.computeCurrentVelocity(1000) - val velX = velocityTracker.xVelocity - val velY = velocityTracker.yVelocity - - // If the object is moving too quickly within the magnetic field, do not stick it. This - // only applies to objects newly stuck to a target. If the object is moved into a new - // target, it wasn't moving at all (since it was stuck to the previous one). - if (objectNewlyStuckToTarget && abs(velX) > stickToTargetMaxXVelocity) { - return false - } - - // This touch event is newly within the magnetic field - let the listener know, and - // animate sticking to the magnet. - targetObjectIsStuckTo = targetObjectIsInMagneticFieldOf - cancelAnimations() - magnetListener.onStuckToTarget(targetObjectIsInMagneticFieldOf!!) - animateStuckToTarget(targetObjectIsInMagneticFieldOf, velX, velY, false, null) - - vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) - } else if (targetObjectIsInMagneticFieldOf == null && objectStuckToTarget) { - velocityTracker.computeCurrentVelocity(1000) - - // This touch event is newly outside the magnetic field - let the listener know. It will - // move the object out of the target using its own movement logic. - cancelAnimations() - magnetListener.onUnstuckFromTarget( - targetObjectIsStuckTo!!, velocityTracker.xVelocity, velocityTracker.yVelocity, - wasFlungOut = false) - targetObjectIsStuckTo = null - - vibrateIfEnabled(VibrationEffect.EFFECT_TICK) - } - - // First, check for relevant gestures concluding with an ACTION_UP. - if (ev.action == MotionEvent.ACTION_UP) { - - velocityTracker.computeCurrentVelocity(1000 /* units */) - val velX = velocityTracker.xVelocity - val velY = velocityTracker.yVelocity - - // Cancel the magnetic animation since we might still be springing into the magnetic - // target, but we're about to fling away or release. - cancelAnimations() - - if (objectStuckToTarget) { - if (-velY > flingUnstuckFromTargetMinVelocity) { - // If the object is stuck, but it was forcefully flung away from the target in - // the upward direction, tell the listener so the object can be animated out of - // the target. - magnetListener.onUnstuckFromTarget( - targetObjectIsStuckTo!!, velX, velY, wasFlungOut = true) - } else { - // If the object is stuck and not flung away, it was released inside the target. - magnetListener.onReleasedInTarget(targetObjectIsStuckTo!!) - vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) - } - - // Either way, we're no longer stuck. - targetObjectIsStuckTo = null - return true - } - - // The target we're flinging towards, or null if we're not flinging towards any target. - val flungToTarget = associatedTargets.firstOrNull { target -> - isForcefulFlingTowardsTarget(target, ev.rawX, ev.rawY, velX, velY) - } - - if (flungToTarget != null) { - // If this is a fling-to-target, animate the object to the magnet and then release - // it. - magnetListener.onStuckToTarget(flungToTarget) - targetObjectIsStuckTo = flungToTarget - - animateStuckToTarget(flungToTarget, velX, velY, true) { - magnetListener.onReleasedInTarget(flungToTarget) - targetObjectIsStuckTo = null - vibrateIfEnabled(VibrationEffect.EFFECT_HEAVY_CLICK) - } - - return true - } - - // If it's not either of those things, we are not interested. - return false - } - - return objectStuckToTarget // Always consume touch events if the object is stuck. - } - - /** Plays the given vibration effect if haptics are enabled. */ - @SuppressLint("MissingPermission") - private fun vibrateIfEnabled(effect: Int) { - if (hapticsEnabled && systemHapticsEnabled) { - vibrator.vibrate(effect.toLong()) - } - } - - /** Adds the movement to the velocity tracker using raw coordinates. */ - private fun addMovement(event: MotionEvent) { - // Add movement to velocity tracker using raw screen X and Y coordinates instead - // of window coordinates because the window frame may be moving at the same time. - val deltaX = event.rawX - event.x - val deltaY = event.rawY - event.y - event.offsetLocation(deltaX, deltaY) - velocityTracker.addMovement(event) - event.offsetLocation(-deltaX, -deltaY) - } - - /** Animates sticking the object to the provided target with the given start velocities. */ - private fun animateStuckToTargetInternal( - target: MagneticTarget, - velX: Float, - velY: Float, - flung: Boolean, - after: (() -> Unit)? = null - ) { - target.updateLocationOnScreen() - getLocationOnScreen(underlyingObject, objectLocationOnScreen) - - // Calculate the difference between the target's center coordinates and the object's. - // Animating the object's x/y properties by these values will center the object on top - // of the magnetic target. - val xDiff = target.centerOnScreen.x - - getWidth(underlyingObject) / 2f - objectLocationOnScreen[0] - val yDiff = target.centerOnScreen.y - - getHeight(underlyingObject) / 2f - objectLocationOnScreen[1] - - val springConfig = if (flung) flungIntoTargetSpringConfig else springConfig - - cancelAnimations() - - // Animate to the center of the target. - animator - .spring(xProperty, xProperty.getValue(underlyingObject) + xDiff, velX, - springConfig) - .spring(yProperty, yProperty.getValue(underlyingObject) + yDiff, velY, - springConfig) - - if (physicsAnimatorUpdateListener != null) { - animator.addUpdateListener(physicsAnimatorUpdateListener!!) - } - - if (physicsAnimatorEndListener != null) { - animator.addEndListener(physicsAnimatorEndListener!!) - } - - if (after != null) { - animator.withEndActions(after) - } - - animator.start() - } - - /** - * Whether or not the provided values match a 'fast fling' towards the provided target. If it - * does, we consider it a fling-to-target gesture. - */ - private fun isForcefulFlingTowardsTarget( - target: MagneticTarget, - rawX: Float, - rawY: Float, - velX: Float, - velY: Float - ): Boolean { - if (!flingToTargetEnabled) { - return false - } - - // Whether velocity is sufficient, depending on whether we're flinging into a target at the - // top or the bottom of the screen. - val velocitySufficient = - if (rawY < target.centerOnScreen.y) velY > flingToTargetMinVelocity - else velY < flingToTargetMinVelocity - - if (!velocitySufficient) { - return false - } - - // Whether the trajectory of the fling intersects the target area. - var targetCenterXIntercept = rawX - - // Only do math if the X velocity is non-zero, otherwise X won't change. - if (velX != 0f) { - // Rise over run... - val slope = velY / velX - // ...y = mx + b, b = y / mx... - val yIntercept = rawY - slope * rawX - - // ...calculate the x value when y = the target's y-coordinate. - targetCenterXIntercept = (target.centerOnScreen.y - yIntercept) / slope - } - - // The width of the area we're looking for a fling towards. - val targetAreaWidth = target.targetView.width * flingToTargetWidthPercent - - // Velocity was sufficient, so return true if the intercept is within the target area. - return targetCenterXIntercept > target.centerOnScreen.x - targetAreaWidth / 2 && - targetCenterXIntercept < target.centerOnScreen.x + targetAreaWidth / 2 - } - - /** Cancel animations on this object's x/y properties. */ - internal fun cancelAnimations() { - animator.cancel(xProperty, yProperty) - } - - /** Updates the locations on screen of all of the [associatedTargets]. */ - internal fun updateTargetViews() { - associatedTargets.forEach { it.updateLocationOnScreen() } - - // Update the touch slop, since the configuration may have changed. - if (associatedTargets.size > 0) { - touchSlop = - ViewConfiguration.get(associatedTargets[0].targetView.context).scaledTouchSlop - } - } - - /** - * Represents a target view with a magnetic field radius and cached center-on-screen - * coordinates. - * - * Instances of MagneticTarget are passed to a MagnetizedObject's [addTarget], and can then - * attract the object if it's dragged near or flung towards it. MagneticTargets can be added to - * multiple objects. - */ - class MagneticTarget( - val targetView: View, - var magneticFieldRadiusPx: Int - ) { - val centerOnScreen = PointF() - - private val tempLoc = IntArray(2) - - fun updateLocationOnScreen() { - targetView.post { - targetView.getLocationOnScreen(tempLoc) - - // Add half of the target size to get the center, and subtract translation since the - // target could be animating in while we're doing this calculation. - centerOnScreen.set( - tempLoc[0] + targetView.width / 2f - targetView.translationX, - tempLoc[1] + targetView.height / 2f - targetView.translationY) - } - } - } - - companion object { - - /** - * Whether the HAPTIC_FEEDBACK_ENABLED setting is true. - * - * We put it in the companion object because we need to register a settings observer and - * [MagnetizedObject] doesn't have an obvious lifecycle so we don't have a good time to - * remove that observer. Since this settings is shared among all instances we just let all - * instances read from this value. - */ - private var systemHapticsEnabled = false - private var hapticSettingObserverInitialized = false - - private fun initHapticSettingObserver(context: Context) { - if (hapticSettingObserverInitialized) { - return - } - - val hapticSettingObserver = - object : ContentObserver(Handler.getMain()) { - override fun onChange(selfChange: Boolean) { - systemHapticsEnabled = - Settings.System.getIntForUser( - context.contentResolver, - Settings.System.HAPTIC_FEEDBACK_ENABLED, - 0, - UserHandle.USER_CURRENT) != 0 - } - } - - context.contentResolver.registerContentObserver( - Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED), - true /* notifyForDescendants */, hapticSettingObserver) - - // Trigger the observer once to initialize systemHapticsEnabled. - hapticSettingObserver.onChange(false /* selfChange */) - hapticSettingObserverInitialized = true - } - - /** - * Magnetizes the given view. Magnetized views are attracted to one or more magnetic - * targets. Magnetic targets attract objects that are dragged near them, and hold them there - * unless they're moved away or released. Releasing objects inside a magnetic target - * typically performs an action on the object. - * - * Magnetized views can also be flung to targets, which will result in the view being pulled - * into the target and released as if it was dragged into it. - * - * To use the returned MagnetizedObject<View> instance, first set [magnetListener] to - * receive event callbacks. In your touch handler, pass all MotionEvents that move this view - * to [maybeConsumeMotionEvent]. If that method returns true, consider the event consumed by - * MagnetizedObject and don't move the view unless it begins returning false again. - * - * The view will be moved via translationX/Y properties, and its - * width/height will be determined via getWidth()/getHeight(). If you are animating - * something other than a view, or want to position your view using properties other than - * translationX/Y, implement an instance of [MagnetizedObject]. - * - * Note that the magnetic library can't re-order your view automatically. If the view - * renders on top of the target views, it will obscure the target when it sticks to it. - * You'll want to bring the view to the front in [MagnetListener.onStuckToTarget]. - */ - @JvmStatic - fun <T : View> magnetizeView(view: T): MagnetizedObject<T> { - return object : MagnetizedObject<T>( - view.context, - view, - DynamicAnimation.TRANSLATION_X, - DynamicAnimation.TRANSLATION_Y) { - override fun getWidth(underlyingObject: T): Float { - return underlyingObject.width.toFloat() - } - - override fun getHeight(underlyingObject: T): Float { - return underlyingObject.height.toFloat() } - - override fun getLocationOnScreen(underlyingObject: T, loc: IntArray) { - underlyingObject.getLocationOnScreen(loc) - } - } - } - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java index aa50292edbf7..71b255229c8f 100644 --- a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java @@ -199,7 +199,7 @@ class ThresholdSensorImpl implements ThresholdSensor { @Override public String toString() { - return String.format("{registered=%s, paused=%s, threshold=%s, sensor=%s}", + return String.format("{isLoaded=%s, registered=%s, paused=%s, threshold=%s, sensor=%s}", isLoaded(), mRegistered, mPaused, mThreshold, mSensor); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 78f83d3c09b4..735338f84267 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -90,6 +90,7 @@ import com.android.settingslib.Utils; import com.android.systemui.Dependency; import com.android.systemui.Prefs; import com.android.systemui.R; +import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialogController; @@ -122,8 +123,11 @@ public class VolumeDialogImpl implements VolumeDialog, static final int DIALOG_SAFETYWARNING_TIMEOUT_MILLIS = 5000; static final int DIALOG_ODI_CAPTIONS_TOOLTIP_TIMEOUT_MILLIS = 5000; static final int DIALOG_HOVERING_TIMEOUT_MILLIS = 16000; - static final int DIALOG_SHOW_ANIMATION_DURATION = 300; - static final int DIALOG_HIDE_ANIMATION_DURATION = 250; + + private final int mDialogShowAnimationDurationMs; + private final int mDialogHideAnimationDurationMs; + private final boolean mShowLowMediaVolumeIcon; + private final boolean mChangeVolumeRowTintWhenInactive; private final Context mContext; private final H mHandler = new H(); @@ -154,8 +158,6 @@ public class VolumeDialogImpl implements VolumeDialog, private boolean mShowing; private boolean mShowA11yStream; - private final boolean mShowLowMediaVolumeIcon; - private int mActiveStream; private int mPrevActiveStream; private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE; @@ -183,6 +185,12 @@ public class VolumeDialogImpl implements VolumeDialog, Prefs.getBoolean(context, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false); mShowLowMediaVolumeIcon = mContext.getResources().getBoolean(R.bool.config_showLowMediaVolumeIcon); + mChangeVolumeRowTintWhenInactive = + mContext.getResources().getBoolean(R.bool.config_changeVolumeRowTintWhenInactive); + mDialogShowAnimationDurationMs = + mContext.getResources().getInteger(R.integer.config_dialogShowAnimationDurationMs); + mDialogHideAnimationDurationMs = + mContext.getResources().getInteger(R.integer.config_dialogHideAnimationDurationMs); } @Override @@ -272,7 +280,7 @@ public class VolumeDialogImpl implements VolumeDialog, mDialogView.animate() .alpha(1) .translationX(0) - .setDuration(DIALOG_SHOW_ANIMATION_DURATION) + .setDuration(mDialogShowAnimationDurationMs) .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator()) .withEndAction(() -> { if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) { @@ -512,6 +520,7 @@ public class VolumeDialogImpl implements VolumeDialog, Events.writeEvent(Events.EVENT_SETTINGS_CLICK); Intent intent = new Intent(Settings.Panel.ACTION_VOLUME); dismissH(DISMISS_REASON_SETTINGS_CLICKED); + Dependency.get(MediaOutputDialogFactory.class).dismiss(); Dependency.get(ActivityStarter.class).startActivity(intent, true /* dismissShade */); }); @@ -592,7 +601,7 @@ public class VolumeDialogImpl implements VolumeDialog, mODICaptionsTooltipView.setAlpha(0.f); mODICaptionsTooltipView.animate() .alpha(1.f) - .setStartDelay(DIALOG_SHOW_ANIMATION_DURATION) + .setStartDelay(mDialogShowAnimationDurationMs) .withEndAction(() -> { if (D.BUG) Log.d(TAG, "tool:checkODICaptionsTooltip() putBoolean true"); Prefs.putBoolean(mContext, @@ -614,7 +623,7 @@ public class VolumeDialogImpl implements VolumeDialog, mODICaptionsTooltipView.animate() .alpha(0.f) .setStartDelay(0) - .setDuration(DIALOG_HIDE_ANIMATION_DURATION) + .setDuration(mDialogHideAnimationDurationMs) .withEndAction(() -> mODICaptionsTooltipView.setVisibility(INVISIBLE)) .start(); } @@ -793,7 +802,7 @@ public class VolumeDialogImpl implements VolumeDialog, mDialogView.setAlpha(1); ViewPropertyAnimator animator = mDialogView.animate() .alpha(0) - .setDuration(DIALOG_HIDE_ANIMATION_DURATION) + .setDuration(mDialogHideAnimationDurationMs) .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator()) .withEndAction(() -> mHandler.postDelayed(() -> { mDialog.dismiss(); @@ -1076,7 +1085,7 @@ public class VolumeDialogImpl implements VolumeDialog, iconRes = isStreamMuted(ss) ? R.drawable.ic_volume_media_bt_mute : R.drawable.ic_volume_media_bt; } else if (isStreamMuted(ss)) { - iconRes = row.iconMuteRes; + iconRes = ss.muted ? R.drawable.ic_volume_media_off : row.iconMuteRes; } else { iconRes = mShowLowMediaVolumeIcon && ss.level * 2 < (ss.levelMax + ss.levelMin) ? R.drawable.ic_volume_media_low : row.iconRes; @@ -1154,6 +1163,9 @@ public class VolumeDialogImpl implements VolumeDialog, row.slider.requestFocus(); } boolean useActiveColoring = isActive && row.slider.isEnabled(); + if (!useActiveColoring && !mChangeVolumeRowTintWhenInactive) { + return; + } final ColorStateList tint = useActiveColoring ? Utils.getColorAccent(mContext) : Utils.getColorAttr(mContext, android.R.attr.colorForeground); diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java new file mode 100644 index 000000000000..f55445ca1de3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvPipModule.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2019 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.wmshell; + +import android.content.Context; +import android.os.Handler; +import android.view.LayoutInflater; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.tv.PipController; +import com.android.wm.shell.pip.tv.PipControlsView; +import com.android.wm.shell.pip.tv.PipControlsViewController; +import com.android.wm.shell.pip.tv.PipNotification; +import com.android.wm.shell.splitscreen.SplitScreen; + +import java.util.Optional; + +import dagger.Module; +import dagger.Provides; + +/** + * Dagger module for TV Pip. + */ +@Module +public abstract class TvPipModule { + + @SysUISingleton + @Provides + static Pip providePipController(Context context, + PipBoundsHandler pipBoundsHandler, + PipTaskOrganizer pipTaskOrganizer, + WindowManagerShellWrapper windowManagerShellWrapper) { + return new PipController(context, pipBoundsHandler, pipTaskOrganizer, + windowManagerShellWrapper); + } + + @SysUISingleton + @Provides + static PipControlsViewController providePipControlsViewContrller( + PipControlsView pipControlsView, PipController pipController, + LayoutInflater layoutInflater, Handler handler) { + return new PipControlsViewController(pipControlsView, pipController, layoutInflater, + handler); + } + + @SysUISingleton + @Provides + static PipControlsView providePipControlsView(Context context) { + return new PipControlsView(context, null); + } + + @SysUISingleton + @Provides + static PipNotification providePipNotification(Context context, + PipController pipController) { + return new PipNotification(context, pipController); + } + + @SysUISingleton + @Provides + static PipBoundsHandler providePipBoundsHandler(Context context) { + return new PipBoundsHandler(context); + } + + @SysUISingleton + @Provides + static PipBoundsState providePipBoundsState() { + return new PipBoundsState(); + } + + @SysUISingleton + @Provides + static PipTaskOrganizer providePipTaskOrganizer(Context context, + PipBoundsState pipBoundsState, + PipBoundsHandler pipBoundsHandler, + PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + Optional<SplitScreen> splitScreenOptional, DisplayController displayController, + PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer) { + return new PipTaskOrganizer(context, pipBoundsState, pipBoundsHandler, + pipSurfaceTransactionHelper, splitScreenOptional, displayController, + pipUiEventLogger, shellTaskOrganizer); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java index 0869cf739d02..56efffc29d85 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/TvWMShellModule.java @@ -20,26 +20,18 @@ import android.content.Context; import android.os.Handler; import android.view.IWindowManager; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.pip.Pip; -import com.android.systemui.pip.PipBoundsHandler; -import com.android.systemui.pip.PipSurfaceTransactionHelper; -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.systemui.pip.PipUiEventLogger; -import com.android.systemui.pip.tv.PipController; -import com.android.systemui.pip.tv.PipNotification; -import com.android.systemui.pip.tv.dagger.TvPipComponent; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; -import java.util.Optional; +import java.util.concurrent.Executor; import dagger.Module; import dagger.Provides; @@ -49,60 +41,23 @@ import dagger.Provides; * branches of SystemUI. */ // TODO(b/162923491): Move most of these dependencies into WMSingleton scope. -@Module(includes = WMShellBaseModule.class, subcomponents = {TvPipComponent.class}) +@Module(includes = {WMShellBaseModule.class, TvPipModule.class}) public class TvWMShellModule { @SysUISingleton @Provides static DisplayImeController provideDisplayImeController(IWindowManager wmService, - DisplayController displayController, @Main Handler mainHandler, + DisplayController displayController, @Main Executor mainExecutor, TransactionPool transactionPool) { - return new DisplayImeController(wmService, displayController, mainHandler, transactionPool); + return new DisplayImeController(wmService, displayController, mainExecutor, + transactionPool); } - @SysUISingleton - @Provides - static Pip providePipController(Context context, - BroadcastDispatcher broadcastDispatcher, - PipBoundsHandler pipBoundsHandler, - PipSurfaceTransactionHelper pipSurfaceTransactionHelper, - PipTaskOrganizer pipTaskOrganizer) { - return new PipController(context, broadcastDispatcher, pipBoundsHandler, - pipSurfaceTransactionHelper, pipTaskOrganizer); - } - - @SysUISingleton - @Provides static SplitScreen provideSplitScreen(Context context, DisplayController displayController, SystemWindows systemWindows, DisplayImeController displayImeController, @Main Handler handler, - TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer) { + TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue) { return new SplitScreenController(context, displayController, systemWindows, - displayImeController, handler, transactionPool, shellTaskOrganizer); - } - - @SysUISingleton - @Provides - static PipNotification providePipNotification(Context context, - BroadcastDispatcher broadcastDispatcher, - PipController pipController) { - return new PipNotification(context, broadcastDispatcher, pipController); - } - - @SysUISingleton - @Provides - static PipBoundsHandler providesPipBoundsHandler(Context context) { - return new PipBoundsHandler(context); - } - - @SysUISingleton - @Provides - static PipTaskOrganizer providesPipTaskOrganizer(Context context, - PipBoundsHandler pipBoundsHandler, - PipSurfaceTransactionHelper pipSurfaceTransactionHelper, - Optional<SplitScreen> splitScreenOptional, DisplayController displayController, - PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer) { - return new PipTaskOrganizer(context, pipBoundsHandler, - pipSurfaceTransactionHelper, splitScreenOptional, displayController, - pipUiEventLogger, shellTaskOrganizer); + displayImeController, handler, transactionPool, shellTaskOrganizer, syncQueue); } } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index ce125f3fdce0..9281a090fd97 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -16,34 +16,52 @@ package com.android.systemui.wmshell; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.ActivityTaskManager.RootTaskInfo; import android.content.ComponentName; import android.content.Context; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.inputmethodservice.InputMethodService; import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pair; import android.view.KeyEvent; import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.Dependency; import com.android.systemui.SystemUI; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationModeController; -import com.android.systemui.pip.Pip; -import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.shared.tracing.ProtoTraceable; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; import com.android.wm.shell.ShellTaskOrganizer; @@ -53,11 +71,12 @@ import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEvents; import com.android.wm.shell.onehanded.OneHandedGestureHandler.OneHandedGestureEventCallback; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.phone.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogImpl; import com.android.wm.shell.splitscreen.SplitScreen; import java.io.FileDescriptor; -import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.Arrays; import java.util.Optional; @@ -70,10 +89,22 @@ import javax.inject.Inject; @SysUISingleton public final class WMShell extends SystemUI implements CommandQueue.Callbacks, ProtoTraceable<SystemUiTraceProto> { + private static final String TAG = WMShell.class.getName(); + private static final int INVALID_SYSUI_STATE_MASK = + SYSUI_STATE_GLOBAL_ACTIONS_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED + | SYSUI_STATE_BOUNCER_SHOWING + | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED + | SYSUI_STATE_BUBBLES_EXPANDED + | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; + private final CommandQueue mCommandQueue; + private final ConfigurationController mConfigurationController; private final DisplayImeController mDisplayImeController; + private final InputConsumerController mInputConsumerController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private final ActivityManagerWrapper mActivityManagerWrapper; + private final TaskStackChangeListeners mTaskStackChangeListeners; private final NavigationModeController mNavigationModeController; private final ScreenLifecycle mScreenLifecycle; private final SysUiState mSysUiState; @@ -84,14 +115,17 @@ public final class WMShell extends SystemUI // are non-optional windowing features like FULLSCREEN. private final ShellTaskOrganizer mShellTaskOrganizer; private final ProtoTracer mProtoTracer; - + private boolean mIsSysUiStateValid; private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback; + private KeyguardUpdateMonitorCallback mPipKeyguardCallback; private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback; @Inject public WMShell(Context context, CommandQueue commandQueue, + ConfigurationController configurationController, + InputConsumerController inputConsumerController, KeyguardUpdateMonitor keyguardUpdateMonitor, - ActivityManagerWrapper activityManagerWrapper, + TaskStackChangeListeners taskStackChangeListeners, DisplayImeController displayImeController, NavigationModeController navigationModeController, ScreenLifecycle screenLifecycle, @@ -103,9 +137,10 @@ public final class WMShell extends SystemUI ProtoTracer protoTracer) { super(context); mCommandQueue = commandQueue; - mCommandQueue.addCallback(this); + mConfigurationController = configurationController; + mInputConsumerController = inputConsumerController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mActivityManagerWrapper = activityManagerWrapper; + mTaskStackChangeListeners = taskStackChangeListeners; mDisplayImeController = displayImeController; mNavigationModeController = navigationModeController; mScreenLifecycle = screenLifecycle; @@ -120,6 +155,7 @@ public final class WMShell extends SystemUI @Override public void start() { + mCommandQueue.addCallback(this); // This is to prevent circular init problem by separating registration step out of its // constructor. And make sure the initialization of DisplayImeController won't depend on // specific feature anymore. @@ -131,12 +167,99 @@ public final class WMShell extends SystemUI @VisibleForTesting void initPip(Pip pip) { + if (!PipUtils.hasSystemFeature(mContext)) { + return; + } mCommandQueue.addCallback(new CommandQueue.Callbacks() { @Override public void showPictureInPictureMenu() { pip.showPictureInPictureMenu(); } }); + + mPipKeyguardCallback = new KeyguardUpdateMonitorCallback() { + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + if (showing) { + pip.hidePipMenu(null, null); + } + } + }; + mKeyguardUpdateMonitor.registerCallback(mPipKeyguardCallback); + + mSysUiState.addCallback(sysUiStateFlag -> { + mIsSysUiStateValid = (sysUiStateFlag & INVALID_SYSUI_STATE_MASK) == 0; + pip.onSystemUiStateChanged(mIsSysUiStateValid, sysUiStateFlag); + }); + + mConfigurationController.addCallback(new ConfigurationController.ConfigurationListener() { + @Override + public void onDensityOrFontScaleChanged() { + pip.onDensityOrFontScaleChanged(); + } + + @Override + public void onOverlayChanged() { + pip.onOverlayChanged(); + } + }); + + // Handle for system task stack changes. + mTaskStackChangeListeners.registerTaskStackListener( + new TaskStackChangeListener() { + @Override + public void onTaskStackChanged() { + pip.onTaskStackChanged(); + } + + @Override + public void onActivityPinned(String packageName, int userId, int taskId, + int stackId) { + pip.onActivityPinned(packageName); + mInputConsumerController.registerInputConsumer(true /* withSfVsync */); + } + + @Override + public void onActivityUnpinned() { + final Pair<ComponentName, Integer> topPipActivityInfo = + PipUtils.getTopPipActivity( + mContext, ActivityManager.getService()); + final ComponentName topActivity = topPipActivityInfo.first; + pip.onActivityUnpinned(topActivity); + mInputConsumerController.unregisterInputConsumer(); + } + + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + pip.onActivityRestartAttempt(task, clearedTask); + } + }); + + try { + RootTaskInfo taskInfo = ActivityTaskManager.getService().getRootTaskInfo( + WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); + if (taskInfo != null) { + // If SystemUI restart, and it already existed a pinned stack, + // register the pip input consumer to ensure touch can send to it. + mInputConsumerController.registerInputConsumer(true /* withSfVsync */); + } + } catch (RemoteException | UnsupportedOperationException e) { + Log.e(TAG, "Failed to register pinned stack listener", e); + e.printStackTrace(); + } + + // Register the listener for input consumer touch events. Only for Phone + if (pip.getPipTouchHandler() != null) { + mInputConsumerController.setInputListener(pip.getPipTouchHandler()::handleTouchEvent); + mInputConsumerController.setRegistrationListener( + pip.getPipTouchHandler()::onRegistrationChanged); + } + + // The media session listener needs to be re-registered when switching users + UserInfoController userInfoController = Dependency.get(UserInfoController.class); + userInfoController.addCallback((String name, Drawable picture, String userAccount) -> + pip.registerSessionListenerForCurrentUser()); } @VisibleForTesting @@ -153,7 +276,7 @@ public final class WMShell extends SystemUI }; mKeyguardUpdateMonitor.registerCallback(mSplitScreenKeyguardCallback); - mActivityManagerWrapper.registerTaskStackListener( + mTaskStackChangeListeners.registerTaskStackListener( new TaskStackChangeListener() { @Override public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, @@ -189,10 +312,6 @@ public final class WMShell extends SystemUI @VisibleForTesting void initOneHanded(OneHanded oneHanded) { - if (!oneHanded.hasOneHandedFeature()) { - return; - } - int currentMode = mNavigationModeController.addListener(mode -> oneHanded.setThreeButtonModeEnabled(mode == NAV_BAR_MODE_3BUTTON)); oneHanded.setThreeButtonModeEnabled(currentMode == NAV_BAR_MODE_3BUTTON); @@ -269,7 +388,7 @@ public final class WMShell extends SystemUI } }); - mActivityManagerWrapper.registerTaskStackListener( + mTaskStackChangeListeners.registerTaskStackListener( new TaskStackChangeListener() { @Override public void onTaskCreated(int taskId, ComponentName componentName) { @@ -300,8 +419,8 @@ public final class WMShell extends SystemUI if (handleLoggingCommand(args, pw)) { return; } - // Dump WMShell stuff here if no commands were handled + mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java index 7c129ac92fe3..09678b5d1772 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java @@ -16,6 +16,7 @@ package com.android.systemui.wmshell; +import android.app.IActivityManager; import android.content.Context; import android.content.pm.PackageManager; import android.os.Handler; @@ -23,22 +24,33 @@ import android.util.DisplayMetrics; import android.view.IWindowManager; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.bubbles.Bubbles; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.pip.Pip; -import com.android.systemui.pip.PipSurfaceTransactionHelper; -import com.android.systemui.pip.PipUiEventLogger; -import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.util.DeviceConfigProxy; -import com.android.systemui.util.FloatingContentCoordinator; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.animation.FlingAnimationUtils; +import com.android.wm.shell.common.AnimationThread; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.HandlerExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.onehanded.OneHanded; +import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.phone.PipAppOpsListener; +import com.android.wm.shell.pip.phone.PipMediaController; +import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.splitscreen.SplitScreen; +import java.util.Optional; + import dagger.BindsOptionalOf; import dagger.Module; import dagger.Provides; @@ -71,12 +83,33 @@ public abstract class WMShellBaseModule { @SysUISingleton @Provides + static InputConsumerController provideInputConsumerController() { + return InputConsumerController.getPipInputConsumer(); + } + + @SysUISingleton + @Provides static FloatingContentCoordinator provideFloatingContentCoordinator() { return new FloatingContentCoordinator(); } @SysUISingleton @Provides + static PipAppOpsListener providePipAppOpsListener(Context context, + IActivityManager activityManager, + PipTouchHandler pipTouchHandler) { + return new PipAppOpsListener(context, activityManager, pipTouchHandler.getMotionHelper()); + } + + @SysUISingleton + @Provides + static PipMediaController providePipMediaController(Context context, + IActivityManager activityManager) { + return new PipMediaController(context, activityManager); + } + + @SysUISingleton + @Provides static PipUiEventLogger providePipUiEventLogger(UiEventLogger uiEventLogger, PackageManager packageManager) { return new PipUiEventLogger(uiEventLogger, packageManager); @@ -84,9 +117,8 @@ public abstract class WMShellBaseModule { @SysUISingleton @Provides - static PipSurfaceTransactionHelper providesPipSurfaceTransactionHelper(Context context, - ConfigurationController configController) { - return new PipSurfaceTransactionHelper(context, configController); + static PipSurfaceTransactionHelper providesPipSurfaceTransactionHelper(Context context) { + return new PipSurfaceTransactionHelper(context); } @SysUISingleton @@ -98,14 +130,29 @@ public abstract class WMShellBaseModule { @SysUISingleton @Provides - static ShellTaskOrganizer provideShellTaskOrganizer(TransactionPool transactionPool) { - ShellTaskOrganizer organizer = new ShellTaskOrganizer(transactionPool); + static SyncTransactionQueue provideSyncTransactionQueue(@Main Handler handler, + TransactionPool pool) { + return new SyncTransactionQueue(pool, handler); + } + + @SysUISingleton + @Provides + static ShellTaskOrganizer provideShellTaskOrganizer(SyncTransactionQueue syncQueue, + @Main Handler handler, TransactionPool transactionPool) { + ShellTaskOrganizer organizer = new ShellTaskOrganizer(syncQueue, transactionPool, + new HandlerExecutor(handler), AnimationThread.instance().getExecutor()); organizer.registerOrganizer(); return organizer; } @SysUISingleton @Provides + static WindowManagerShellWrapper provideWindowManagerShellWrapper() { + return new WindowManagerShellWrapper(); + } + + @SysUISingleton + @Provides static FlingAnimationUtils.Builder provideFlingAnimationUtilsBuilder( DisplayMetrics displayMetrics) { return new FlingAnimationUtils.Builder(displayMetrics); @@ -118,5 +165,12 @@ public abstract class WMShellBaseModule { abstract SplitScreen optionalSplitScreen(); @BindsOptionalOf - abstract OneHanded optionalOneHanded(); + abstract Bubbles optionalBubbles(); + + @SysUISingleton + @Provides + static Optional<OneHanded> provideOneHandedController(Context context, + DisplayController displayController) { + return Optional.ofNullable(OneHandedController.create(context, displayController)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java index 16fb2cacc950..975757a4c259 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java @@ -20,30 +20,32 @@ import android.content.Context; import android.os.Handler; import android.view.IWindowManager; -import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.model.SysUiState; -import com.android.systemui.pip.Pip; -import com.android.systemui.pip.PipBoundsHandler; -import com.android.systemui.pip.PipSurfaceTransactionHelper; -import com.android.systemui.pip.PipTaskOrganizer; -import com.android.systemui.pip.PipUiEventLogger; -import com.android.systemui.pip.phone.PipController; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.util.DeviceConfigProxy; -import com.android.systemui.util.FloatingContentCoordinator; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.onehanded.OneHanded; -import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipBoundsHandler; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.phone.PipAppOpsListener; +import com.android.wm.shell.pip.phone.PipController; +import com.android.wm.shell.pip.phone.PipMediaController; +import com.android.wm.shell.pip.phone.PipMenuActivityController; +import com.android.wm.shell.pip.phone.PipTouchHandler; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import java.util.Optional; +import java.util.concurrent.Executor; import dagger.Module; import dagger.Provides; @@ -58,29 +60,28 @@ public class WMShellModule { @SysUISingleton @Provides static DisplayImeController provideDisplayImeController(IWindowManager wmService, - DisplayController displayController, @Main Handler mainHandler, + DisplayController displayController, @Main Executor mainExecutor, TransactionPool transactionPool) { - return new DisplayImeController(wmService, displayController, mainHandler, transactionPool); + return new DisplayImeController(wmService, displayController, mainExecutor, + transactionPool); } @SysUISingleton @Provides static Pip providePipController(Context context, - BroadcastDispatcher broadcastDispatcher, - ConfigurationController configController, - DeviceConfigProxy deviceConfig, DisplayController displayController, - FloatingContentCoordinator floatingContentCoordinator, - SysUiState sysUiState, + PipAppOpsListener pipAppOpsListener, PipBoundsHandler pipBoundsHandler, - PipSurfaceTransactionHelper surfaceTransactionHelper, + PipBoundsState pipBoundsState, + PipMediaController pipMediaController, + PipMenuActivityController pipMenuActivityController, PipTaskOrganizer pipTaskOrganizer, - PipUiEventLogger pipUiEventLogger) { - return new PipController(context, broadcastDispatcher, configController, deviceConfig, - displayController, floatingContentCoordinator, sysUiState, pipBoundsHandler, - surfaceTransactionHelper, - pipTaskOrganizer, - pipUiEventLogger); + PipTouchHandler pipTouchHandler, + WindowManagerShellWrapper windowManagerShellWrapper) { + return new PipController(context, displayController, + pipAppOpsListener, pipBoundsHandler, pipBoundsState, pipMediaController, + pipMenuActivityController, pipTaskOrganizer, pipTouchHandler, + windowManagerShellWrapper); } @SysUISingleton @@ -88,9 +89,16 @@ public class WMShellModule { static SplitScreen provideSplitScreen(Context context, DisplayController displayController, SystemWindows systemWindows, DisplayImeController displayImeController, @Main Handler handler, - TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer) { + TransactionPool transactionPool, ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue) { return new SplitScreenController(context, displayController, systemWindows, - displayImeController, handler, transactionPool, shellTaskOrganizer); + displayImeController, handler, transactionPool, shellTaskOrganizer, syncQueue); + } + + @SysUISingleton + @Provides + static PipBoundsState providePipBoundsState() { + return new PipBoundsState(); } @SysUISingleton @@ -101,20 +109,33 @@ public class WMShellModule { @SysUISingleton @Provides + static PipMenuActivityController providesPipMenuActivityController(Context context, + PipMediaController pipMediaController, PipTaskOrganizer pipTaskOrganizer) { + return new PipMenuActivityController(context, pipMediaController, pipTaskOrganizer); + } + + @SysUISingleton + @Provides + static PipTouchHandler providesPipTouchHandler(Context context, + PipMenuActivityController menuActivityController, PipBoundsHandler pipBoundsHandler, + PipBoundsState pipBoundsState, + PipTaskOrganizer pipTaskOrganizer, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger) { + return new PipTouchHandler(context, menuActivityController, pipBoundsHandler, + pipBoundsState, pipTaskOrganizer, floatingContentCoordinator, pipUiEventLogger); + } + + @SysUISingleton + @Provides static PipTaskOrganizer providesPipTaskOrganizer(Context context, + PipBoundsState pipBoundsState, PipBoundsHandler pipBoundsHandler, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, Optional<SplitScreen> splitScreenOptional, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer) { - return new PipTaskOrganizer(context, pipBoundsHandler, + return new PipTaskOrganizer(context, pipBoundsState, pipBoundsHandler, pipSurfaceTransactionHelper, splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer); } - - @SysUISingleton - @Provides - static OneHanded provideOneHandedController(Context context, - DisplayController displayController) { - return OneHandedController.create(context, displayController); - } } |