Idle Mode Support.

This changelist introduces idle mode, a user interface
shown when user interaction times out on the lockscreen.
A host view and accompanying controller have been added
to handle the relevant lifecycles inside the notification
view panel.

Test: manual
Bug: 194230499
Change-Id: I5ce5fc790b93be7aa036faf8caa569de7cae8379
This commit is contained in:
Bryce Lee
2021-07-28 14:07:52 -07:00
parent 5f327a6c25
commit a47fea55fc
10 changed files with 492 additions and 5 deletions

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<com.android.systemui.idle.IdleHostView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/idle_host_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@@ -73,7 +73,12 @@
layout="@layout/keyguard_status_view"
android:visibility="gone"/>
<include layout="@layout/dock_info_overlay" />
<include layout="@layout/idle_host_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<include layout="@layout/dock_info_overlay"/>
<FrameLayout
android:id="@+id/qs_frame"

View File

@@ -684,4 +684,11 @@
<!-- Component name of communal source service -->
<string name="config_communalSourceComponent" translatable="false">@null</string>
<!-- Whether idle mode should be enabled. When enabled, the lock screen will timeout to an idle
screen on inactivity. -->
<bool name="config_enableIdleMode">false</bool>
<!-- Timeout to idle mode duration in milliseconds. -->
<integer name="config_idleModeTimeout">10000</integer>
</resources>

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<IdleHostView> {
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<View> mIdleViewProvider;
@Inject
protected IdleHostViewController(
IdleHostView view, InputMonitorFactory factory,
@Main DelayableExecutor delayableExecutor,
@Main Resources resources,
@Main Looper looper,
@Named(IDLE_VIEW) Provider<View> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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,