diff --git a/packages/SystemUI/res/layout/idle_host_view.xml b/packages/SystemUI/res/layout/idle_host_view.xml new file mode 100644 index 0000000000000..f4078742c916a --- /dev/null +++ b/packages/SystemUI/res/layout/idle_host_view.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml index da0010e9ddffc..281eee65ec544 100644 --- a/packages/SystemUI/res/layout/status_bar_expanded.xml +++ b/packages/SystemUI/res/layout/status_bar_expanded.xml @@ -73,7 +73,12 @@ layout="@layout/keyguard_status_view" android:visibility="gone"/> - + + + @null + + + false + + + 10000 diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java index bcc36847d1fe2..5f75d86a94637 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.java @@ -16,13 +16,32 @@ package com.android.systemui.communal.dagger; +import android.content.Context; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.systemui.idle.dagger.IdleViewComponent; + +import javax.inject.Named; + import dagger.Module; +import dagger.Provides; /** * Dagger Module providing Communal-related functionality. */ @Module(subcomponents = { CommunalViewComponent.class, + IdleViewComponent.class, }) -public class CommunalModule { +public interface CommunalModule { + String IDLE_VIEW = "idle_view"; + + /** */ + @Provides + @Named(IDLE_VIEW) + static View provideIdleView(Context context) { + FrameLayout view = new FrameLayout(context); + return view; + } } diff --git a/packages/SystemUI/src/com/android/systemui/idle/IdleHostView.java b/packages/SystemUI/src/com/android/systemui/idle/IdleHostView.java new file mode 100644 index 0000000000000..7d79279799ea1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/idle/IdleHostView.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.idle; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * {@link IdleHostView} houses a surface to be displayed when the device idle. + */ +public class IdleHostView extends FrameLayout { + public IdleHostView(@NonNull Context context) { + this(context, null); + } + + public IdleHostView(@NonNull Context context, + @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public IdleHostView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/idle/IdleHostViewController.java b/packages/SystemUI/src/com/android/systemui/idle/IdleHostViewController.java new file mode 100644 index 0000000000000..78d1045837a19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/idle/IdleHostViewController.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.idle; + +import static com.android.systemui.communal.dagger.CommunalModule.IDLE_VIEW; + +import android.annotation.IntDef; +import android.content.res.Resources; +import android.os.Looper; +import android.util.Log; +import android.view.Choreographer; +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shared.system.InputMonitorCompat; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.ViewController; +import com.android.systemui.util.concurrency.DelayableExecutor; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +/** + * {@link IdleHostViewController} processes signals to control the lifecycle of the idle screen. + */ +public class IdleHostViewController extends ViewController { + private static final String INPUT_MONITOR_IDENTIFIER = "IdleHostViewController"; + private static final String TAG = "IdleHostViewController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + @Retention(RetentionPolicy.RUNTIME) + @IntDef({STATE_IDLE_MODE_ENABLED, STATE_DOZING, STATE_KEYGUARD_SHOWING, STATE_IDLING}) + public @interface State {} + + // Set at construction to indicate idle mode is available. + private static final int STATE_IDLE_MODE_ENABLED = 1 << 0; + + // Set when the device has entered a system level dream. + private static final int STATE_DOZING = 1 << 1; + + // Set when the keyguard is showing. + private static final int STATE_KEYGUARD_SHOWING = 1 << 2; + + // Set when input monitoring has established the device is now idling. + private static final int STATE_IDLING = 1 << 3; + + // The state the controller must be in to start recognizing idleness (lack of input + // interaction). + private static final int CONDITIONS_IDLE_MONITORING = + STATE_IDLE_MODE_ENABLED | STATE_KEYGUARD_SHOWING; + + // The state the controller must be in before entering idle mode. + private static final int CONDITIONS_IDLING = CONDITIONS_IDLE_MONITORING | STATE_IDLING; + + // The aggregate current state. + private int mState; + private boolean mIdleModeActive; + + // Timeout to idle in milliseconds. + private final int mIdleTimeout; + + // Factory for generating input listeners. + private final InputMonitorFactory mInputMonitorFactory; + + // Delayable executor. + private final DelayableExecutor mDelayableExecutor; + + // Runnable for canceling enabling idle. + private Runnable mCancelEnableIdling; + + // Keyguard state controller for monitoring keyguard show state. + private final KeyguardStateController mKeyguardStateController; + + // Status bar state controller for monitoring when the device is dozing. + private final StatusBarStateController mStatusBarStateController; + + // Looper to use for monitoring input. + private final Looper mLooper; + + // Choreographer to use for monitoring input. + private final Choreographer mChoreographer; + + // Monitor for tracking touches for activity. + private InputMonitorCompat mInputMonitor; + + // Delayed callback for enabling idle mode. + private final Runnable mEnableIdlingCallback = () -> { + if (DEBUG) { + Log.d(TAG, "enabling idle"); + } + setState(STATE_IDLING, true); + }; + + private final KeyguardStateController.Callback mKeyguardCallback = + new KeyguardStateController.Callback() { + @Override + public void onKeyguardShowingChanged() { + setState(STATE_KEYGUARD_SHOWING, mKeyguardStateController.isShowing()); + } + }; + + private final StatusBarStateController.StateListener mStatusBarCallback = + new StatusBarStateController.StateListener() { + @Override + public void onDozingChanged(boolean isDozing) { + setState(STATE_DOZING, isDozing); + } + }; + + final Provider mIdleViewProvider; + + @Inject + protected IdleHostViewController( + IdleHostView view, InputMonitorFactory factory, + @Main DelayableExecutor delayableExecutor, + @Main Resources resources, + @Main Looper looper, + @Named(IDLE_VIEW) Provider idleViewProvider, + Choreographer choreographer, + KeyguardStateController keyguardStateController, + StatusBarStateController statusBarStateController) { + super(view); + mIdleViewProvider = idleViewProvider; + mKeyguardStateController = keyguardStateController; + mStatusBarStateController = statusBarStateController; + mLooper = looper; + mChoreographer = choreographer; + + final boolean enabled = resources.getBoolean(R.bool.config_enableIdleMode); + if (enabled) { + mState |= STATE_IDLE_MODE_ENABLED; + } + + setState(mState, true); + + mIdleTimeout = resources.getInteger(R.integer.config_idleModeTimeout); + mInputMonitorFactory = factory; + mDelayableExecutor = delayableExecutor; + + if (DEBUG) { + Log.d(TAG, "initial state:" + mState + " enabled:" + enabled + + " timeout:" + mIdleTimeout); + } + } + + @Override + public void init() { + super.init(); + + setState(STATE_KEYGUARD_SHOWING, mKeyguardStateController.isShowing()); + setState(STATE_DOZING, mStatusBarStateController.isDozing()); + } + + private void setState(@State int state, boolean active) { + final int oldState = mState; + + if (active) { + mState |= state; + } else { + mState &= ~state; + } + + // If we have entered doze or no longer match the preconditions for idling, remove idling. + if ((mState & STATE_DOZING) == STATE_DOZING + || (mState & CONDITIONS_IDLE_MONITORING) != CONDITIONS_IDLE_MONITORING) { + mState &= ~STATE_IDLING; + } + + if (oldState == mState) { + return; + } + + if (DEBUG) { + Log.d(TAG, "state changed from " + oldState + " to " + mState); + } + + enableIdleMonitoring(mState == CONDITIONS_IDLE_MONITORING); + enableIdleMode(mState == CONDITIONS_IDLING); + } + + private void enableIdleMonitoring(boolean enable) { + if (enable && mInputMonitor == null) { + if (DEBUG) { + Log.d(TAG, "enable idle monitoring"); + } + // Set initial timeout to idle. + mCancelEnableIdling = mDelayableExecutor.executeDelayed(mEnableIdlingCallback, + mIdleTimeout); + + // Monitor - any input should reset timer + mInputMonitor = mInputMonitorFactory.getInputMonitor(INPUT_MONITOR_IDENTIFIER); + mInputMonitor.getInputReceiver(mLooper, mChoreographer, + v -> { + if (DEBUG) { + Log.d(TAG, "touch detected, reseting timeout"); + } + // When input is received, reset timeout. + if (mCancelEnableIdling != null) { + mCancelEnableIdling.run(); + mCancelEnableIdling = null; + } + mCancelEnableIdling = mDelayableExecutor.executeDelayed( + mEnableIdlingCallback, mIdleTimeout); + }); + } else if (!enable && mInputMonitor != null) { + if (DEBUG) { + Log.d(TAG, "disable idle monitoring"); + } + // Clean up idle callback and touch monitoring. + if (mCancelEnableIdling != null) { + mCancelEnableIdling.run(); + mCancelEnableIdling = null; + } + + mInputMonitor.dispose(); + mInputMonitor = null; + } + } + + private void enableIdleMode(boolean enable) { + if (mIdleModeActive == enable) { + return; + } + + if (DEBUG) { + Log.d(TAG, "enable idle mode:" + enable); + } + + mIdleModeActive = enable; + mDelayableExecutor.execute(() -> { + mView.setVisibility(mIdleModeActive ? View.VISIBLE : View.GONE); + + if (mIdleModeActive) { + final View idleView = mIdleViewProvider.get(); + idleView.setLayoutParams( + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mView.addView(idleView); + } else { + mView.removeAllViews(); + } + }); + } + + @Override + protected void onViewAttached() { + if (DEBUG) { + Log.d(TAG, "onViewAttached"); + } + + mKeyguardStateController.addCallback(mKeyguardCallback); + mStatusBarStateController.addCallback(mStatusBarCallback); + } + + @Override + protected void onViewDetached() { + mKeyguardStateController.removeCallback(mKeyguardCallback); + mStatusBarStateController.removeCallback(mStatusBarCallback); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/idle/InputMonitorFactory.java b/packages/SystemUI/src/com/android/systemui/idle/InputMonitorFactory.java new file mode 100644 index 0000000000000..be0a378d10a54 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/idle/InputMonitorFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.idle; + +import com.android.systemui.dagger.qualifiers.DisplayId; +import com.android.systemui.shared.system.InputMonitorCompat; + +import javax.inject.Inject; + +/** + * {@link InputMonitorFactory} generates instances of {@link InputMonitorCompat}. + */ +public class InputMonitorFactory { + private final int mDisplayId; + + @Inject + public InputMonitorFactory(@DisplayId int displayId) { + mDisplayId = displayId; + } + + /** + * Generates a new {@link InputMonitorCompat}. + * + * @param identifier Identifier to generate monitor with. + * @return A {@link InputMonitorCompat} instance. + */ + public InputMonitorCompat getInputMonitor(String identifier) { + return new InputMonitorCompat(identifier, mDisplayId); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/idle/dagger/IdleViewComponent.java b/packages/SystemUI/src/com/android/systemui/idle/dagger/IdleViewComponent.java new file mode 100644 index 0000000000000..9754b1f009f5f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/idle/dagger/IdleViewComponent.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.idle.dagger; + +import com.android.systemui.idle.IdleHostView; +import com.android.systemui.idle.IdleHostViewController; + +import dagger.BindsInstance; +import dagger.Subcomponent; + +/** + * Subcomponent for working with {@link IdleHostView}. + */ +@Subcomponent +public interface IdleViewComponent { + /** Simple factory for {@link Factory}. */ + @Subcomponent.Factory + interface Factory { + IdleViewComponent build(@BindsInstance IdleHostView idleHostView); + } + + /** Builds a {@link IdleHostViewController}. */ + IdleHostViewController getIdleHostViewController(); +} 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 98d31ab68bb47..358da4069df8a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -117,6 +117,9 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeLog; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.idle.IdleHostView; +import com.android.systemui.idle.IdleHostViewController; +import com.android.systemui.idle.dagger.IdleViewComponent; import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.media.MediaDataManager; import com.android.systemui.media.MediaHierarchyManager; @@ -332,6 +335,7 @@ public class NotificationPanelViewController extends PanelViewController { private final KeyguardQsUserSwitchComponent.Factory mKeyguardQsUserSwitchComponentFactory; private final KeyguardUserSwitcherComponent.Factory mKeyguardUserSwitcherComponentFactory; private final KeyguardStatusBarViewComponent.Factory mKeyguardStatusBarViewComponentFactory; + private final IdleViewComponent.Factory mIdleViewComponentFactory; private final QSDetailDisplayer mQSDetailDisplayer; private final FragmentService mFragmentService; private final ScrimController mScrimController; @@ -365,6 +369,8 @@ public class NotificationPanelViewController extends PanelViewController { @Nullable private CommunalHostViewController mCommunalViewController; private KeyguardStatusViewController mKeyguardStatusViewController; + @Nullable + private IdleHostViewController mIdleHostViewController; private LockIconViewController mLockIconViewController; private NotificationsQuickSettingsContainer mNotificationContainerParent; private boolean mAnimateNextPositionUpdate; @@ -745,6 +751,7 @@ public class NotificationPanelViewController extends PanelViewController { KeyguardUserSwitcherComponent.Factory keyguardUserSwitcherComponentFactory, KeyguardStatusBarViewComponent.Factory keyguardStatusBarViewComponentFactory, CommunalViewComponent.Factory communalViewComponentFactory, + IdleViewComponent.Factory idleViewComponentFactory, LockscreenShadeTransitionController lockscreenShadeTransitionController, QSDetailDisplayer qsDetailDisplayer, NotificationGroupManagerLegacy groupManager, @@ -792,6 +799,7 @@ public class NotificationPanelViewController extends PanelViewController { mCommunalViewComponentFactory = communalViewComponentFactory; mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory; mKeyguardStatusBarViewComponentFactory = keyguardStatusBarViewComponentFactory; + mIdleViewComponentFactory = idleViewComponentFactory; mDepthController = notificationShadeDepthController; mContentResolver = contentResolver; mKeyguardQsUserSwitchComponentFactory = keyguardQsUserSwitchComponentFactory; @@ -914,7 +922,8 @@ public class NotificationPanelViewController extends PanelViewController { userAvatarView, mKeyguardStatusBar, keyguardUserSwitcherView, - mCommunalView); + mCommunalView, + mView.findViewById(R.id.idle_host_view)); mNotificationContainerParent = mView.findViewById(R.id.notification_container_parent); NotificationStackScrollLayout stackScrollLayout = mView.findViewById( R.id.notification_stack_scroller); @@ -1007,13 +1016,18 @@ public class NotificationPanelViewController extends PanelViewController { UserAvatarView userAvatarView, KeyguardStatusBarView keyguardStatusBarView, KeyguardUserSwitcherView keyguardUserSwitcherView, - CommunalHostView communalView) { + CommunalHostView communalView, + IdleHostView idleHostView) { // Re-associate the KeyguardStatusViewController KeyguardStatusViewComponent statusViewComponent = mKeyguardStatusViewComponentFactory.build(keyguardStatusView); mKeyguardStatusViewController = statusViewComponent.getKeyguardStatusViewController(); mKeyguardStatusViewController.init(); + IdleViewComponent idleViewComponent = mIdleViewComponentFactory.build(idleHostView); + mIdleHostViewController = idleViewComponent.getIdleHostViewController(); + mIdleHostViewController.init(); + KeyguardStatusBarViewComponent statusBarViewComponent = mKeyguardStatusBarViewComponentFactory.build(keyguardStatusBarView); if (mKeyguardStatusBarViewController != null) { @@ -1191,7 +1205,8 @@ public class NotificationPanelViewController extends PanelViewController { mBigClockContainer.removeAllViews(); updateViewControllers(mView.findViewById(R.id.keyguard_status_view), userAvatarView, - mKeyguardStatusBar, keyguardUserSwitcherView, mCommunalView); + mKeyguardStatusBar, keyguardUserSwitcherView, mCommunalView, + mView.findViewById(R.id.idle_host_view)); // Update keyguard bottom area int index = mView.indexOfChild(mKeyguardBottomArea); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java index 20b61926a0f0f..13fb44bf2166f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewTest.java @@ -97,6 +97,8 @@ import com.android.systemui.controls.dagger.ControlsComponent; import com.android.systemui.doze.DozeLog; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.idle.IdleHostViewController; +import com.android.systemui.idle.dagger.IdleViewComponent; import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.media.MediaDataManager; import com.android.systemui.media.MediaHierarchyManager; @@ -245,6 +247,12 @@ public class NotificationPanelViewTest extends SysuiTestCase { @Mock private KeyguardUserSwitcherComponent.Factory mKeyguardUserSwitcherComponentFactory; @Mock + private IdleViewComponent.Factory mIdleViewComponentFactory; + @Mock + private IdleViewComponent mIdleViewComponent; + @Mock + private IdleHostViewController mIdleHostViewController; + @Mock private QSDetailDisplayer mQSDetailDisplayer; @Mock private KeyguardStatusViewComponent mKeyguardStatusViewComponent; @@ -416,6 +424,10 @@ public class NotificationPanelViewTest extends SysuiTestCase { .thenReturn(mCommunalViewComponent); when(mCommunalViewComponent.getCommunalHostViewController()) .thenReturn(mCommunalHostViewController); + when(mIdleViewComponentFactory.build(any())) + .thenReturn(mIdleViewComponent); + when(mIdleViewComponent.getIdleHostViewController()) + .thenReturn(mIdleHostViewController); when(mLayoutInflater.inflate(eq(R.layout.keyguard_status_view), any(), anyBoolean())) .thenReturn(mKeyguardStatusView); when(mLayoutInflater.inflate(eq(R.layout.keyguard_bottom_area), any(), anyBoolean())) @@ -444,6 +456,7 @@ public class NotificationPanelViewTest extends SysuiTestCase { mKeyguardUserSwitcherComponentFactory, mKeyguardStatusBarViewComponentFactory, mCommunalViewComponentFactory, + mIdleViewComponentFactory, mLockscreenShadeTransitionController, mQSDetailDisplayer, mGroupManager,