Use abstractions to allow FullscreenUserSwitcher take advantage of SystemUIOverlayWindow.

This change includes the following:
* Rename SystemUIPrimaryWindow to SystemUIOverlayWindow
* Create Mediator and View controller abstractions that allows
developers to easily take advantage of SystemUIOverlayWindow that is
managed by a single SystemUI Object: SystemUIOverlayWindowManager.
* Convert FullscreenUserSwitcher to take advantage of the newly added
abstractions.

Bug: 147826738
Test: Manual
Change-Id: I19825da81f8d1b1259a2ba115e0238a9ffa69e37
Merged-In: I19825da81f8d1b1259a2ba115e0238a9ffa69e37
This commit is contained in:
Abhijoy Saha
2020-01-14 12:31:00 -08:00
committed by kwaky
parent e6750fd522
commit 84590ea25a
26 changed files with 1385 additions and 371 deletions

View File

@@ -16,6 +16,7 @@
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fullscreen_user_switcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
@@ -24,22 +25,26 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:orientation="vertical">
<include
layout="@layout/car_status_bar_header"
android:layout_alignParentTop="true"
android:theme="@android:style/Theme"/>
<!-- TODO(b/150302361): Status bar is commented out since a top inset is being added which causes it to be displayed below the top of the screen. -->
<!-- <include
layout="@layout/car_status_bar_header"
android:layout_alignParentTop="true"
android:theme="@android:style/Theme"/>-->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.android.systemui.statusbar.car.UserGridRecyclerView
<com.android.systemui.car.userswitcher.UserGridRecyclerView
android:id="@+id/user_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="@dimen/car_user_switcher_margin_top"/>
android:layout_gravity="center_vertical"/>
<!-- TODO(b/150302361): Re-add marginTop once status bar has been added back. -->
<!-- android:layout_marginTop="@dimen/car_user_switcher_margin_top"/>-->
</FrameLayout>
</LinearLayout>

View File

@@ -33,7 +33,7 @@
android:layout_width="match_parent"
android:layout_height="@dimen/car_user_switcher_container_height">
<com.android.systemui.statusbar.car.UserGridRecyclerView
<com.android.systemui.car.userswitcher.UserGridRecyclerView
android:id="@+id/user_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@@ -58,6 +58,11 @@
to a constant alpha percent value using the initial alpha. -->
<integer name="config_finalNotificationBackgroundAlpha">100</integer>
<!-- Car System UI's OverlayViewsMediator-->
<string-array name="config_carSystemUIOverlayViewsMediators" translatable="false">
<item>com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator</item>
</string-array>
<!-- SystemUI Services: The classes of the stuff to start. -->
<string-array name="config_systemUIServiceComponents" translatable="false">
<item>com.android.systemui.util.NotificationChannels</item>
@@ -85,5 +90,6 @@
<item>com.android.systemui.navigationbar.car.CarNavigationBar</item>
<item>com.android.systemui.toast.ToastUI</item>
<item>com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier</item>
<item>com.android.systemui.window.SystemUIOverlayWindowManager</item>
</string-array>
</resources>

View File

@@ -39,6 +39,8 @@ import com.android.systemui.toast.ToastUI;
import com.android.systemui.util.leak.GarbageMonitor;
import com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier;
import com.android.systemui.volume.VolumeUI;
import com.android.systemui.window.OverlayWindowModule;
import com.android.systemui.window.SystemUIOverlayWindowManager;
import dagger.Binds;
import dagger.Module;
@@ -47,7 +49,7 @@ import dagger.multibindings.IntoMap;
/** Binder for car specific {@link SystemUI} modules. */
@Module(includes = {RecentsModule.class, CarStatusBarModule.class, NotificationsModule.class,
BubbleModule.class, KeyguardModule.class})
BubbleModule.class, KeyguardModule.class, OverlayWindowModule.class})
public abstract class CarSystemUIBinder {
/** Inject into AuthController. */
@Binds
@@ -182,4 +184,10 @@ public abstract class CarSystemUIBinder {
@ClassKey(ConnectedDeviceVoiceRecognitionNotifier.class)
public abstract SystemUI bindConnectedDeviceVoiceRecognitionNotifier(
ConnectedDeviceVoiceRecognitionNotifier sysui);
/** Inject into SystemUIOverlayWindowManager. */
@Binds
@IntoMap
@ClassKey(SystemUIOverlayWindowManager.class)
public abstract SystemUI bindSystemUIPrimaryWindowManager(SystemUIOverlayWindowManager sysui);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 The Android Open Source Project
* 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.car;
package com.android.systemui.car.userswitcher;
import android.app.admin.DevicePolicyManager;
import android.bluetooth.BluetoothAdapter;
@@ -56,8 +56,8 @@ class CarTrustAgentUnlockDialogHelper extends BroadcastReceiver{
private final UserManager mUserManager;
private final WindowManager.LayoutParams mParams;
/**
* Not using Dialog because context passed from {@link FullscreenUserSwitcher} is not an
* activity.
* Not using Dialog because context passed from {@link FullscreenUserSwitcherViewMediator}
* is not an activity.
*/
private final View mUnlockDialogLayout;
private final TextView mUnlockingText;

View File

@@ -0,0 +1,124 @@
/*
* 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.car.userswitcher;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.Resources;
import android.view.View;
import androidx.recyclerview.widget.GridLayoutManager;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.window.OverlayViewController;
import com.android.systemui.window.OverlayViewGlobalStateController;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Controller for {@link R.layout#car_fullscreen_user_switcher}.
*/
@Singleton
public class FullScreenUserSwitcherViewController extends OverlayViewController {
private final Context mContext;
private final Resources mResources;
private final int mShortAnimationDuration;
private UserGridRecyclerView mUserGridView;
private UserGridRecyclerView.UserSelectionListener mUserSelectionListener;
@Inject
public FullScreenUserSwitcherViewController(
Context context,
@Main Resources resources,
OverlayViewGlobalStateController overlayViewGlobalStateController) {
super(R.id.fullscreen_user_switcher_stub, overlayViewGlobalStateController);
mContext = context;
mResources = resources;
mShortAnimationDuration = mResources.getInteger(android.R.integer.config_shortAnimTime);
}
@Override
protected void onFinishInflate() {
// Initialize user grid.
mUserGridView = getLayout().findViewById(R.id.user_grid);
GridLayoutManager layoutManager = new GridLayoutManager(mContext,
mResources.getInteger(R.integer.user_fullscreen_switcher_num_col));
mUserGridView.setLayoutManager(layoutManager);
mUserGridView.buildAdapter();
mUserGridView.setUserSelectionListener(mUserSelectionListener);
}
@Override
protected void showInternal() {
getLayout().setVisibility(View.VISIBLE);
}
@Override
protected void hideInternal() {
// Switching is about to happen, since it takes time, fade out the switcher gradually.
fadeOut();
}
private void fadeOut() {
mUserGridView.animate()
.alpha(0.0f)
.setDuration(mShortAnimationDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
getLayout().setVisibility(View.GONE);
mUserGridView.setAlpha(1.0f);
}
});
}
/**
* Invalidate underlying view.
*/
void invalidate() {
if (getLayout() == null) {
// layout hasn't been inflated.
return;
}
getLayout().invalidate();
}
/**
* Set {@link UserGridRecyclerView.UserSelectionListener}.
*/
void setUserGridSelectionListener(
UserGridRecyclerView.UserSelectionListener userGridSelectionListener) {
mUserSelectionListener = userGridSelectionListener;
}
/**
* Returns {@code true} when layout is visible.
*/
boolean isVisible() {
if (getLayout() == null) {
// layout hasn't been inflated.
return false;
}
return getLayout().getVisibility() == View.VISIBLE;
}
}

View File

@@ -0,0 +1,291 @@
/*
* 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.car.userswitcher;
import android.car.Car;
import android.car.trust.CarTrustAgentEnrollmentManager;
import android.car.userlib.CarUserManagerHelper;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import com.android.systemui.car.CarServiceProvider;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.car.CarStatusBar;
import com.android.systemui.statusbar.car.CarStatusBarKeyguardViewManager;
import com.android.systemui.window.OverlayViewMediator;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Manages the fullscreen user switcher and it's interactions with the keyguard.
*/
@Singleton
public class FullscreenUserSwitcherViewMediator implements OverlayViewMediator {
private static final String TAG = FullscreenUserSwitcherViewMediator.class.getSimpleName();
private final Context mContext;
private final UserManager mUserManager;
private final CarServiceProvider mCarServiceProvider;
private final CarTrustAgentUnlockDialogHelper mUnlockDialogHelper;
private final CarStatusBarKeyguardViewManager mCarStatusBarKeyguardViewManager;
private final Handler mMainHandler;
private final StatusBarStateController mStatusBarStateController;
private final FullScreenUserSwitcherViewController mFullScreenUserSwitcherViewController;
private final ScreenLifecycle mScreenLifecycle;
private final CarStatusBar mCarStatusBar;
private final boolean mIsUserSwitcherEnabled;
private final CarUserManagerHelper mCarUserManagerHelper;
private CarTrustAgentEnrollmentManager mEnrollmentManager;
private UserGridRecyclerView.UserRecord mSelectedUser;
private final CarTrustAgentUnlockDialogHelper.OnHideListener mOnHideListener =
dismissUserSwitcher -> {
if (dismissUserSwitcher) {
dismissUserSwitcher();
} else {
// Re-draw the parent view, otherwise the unlock dialog will not be removed
// from the screen immediately.
invalidateFullscreenUserSwitcherView();
}
};
private final BroadcastReceiver mUserUnlockReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "user 0 is unlocked, SharedPreference is accessible.");
}
showDialogForInitialUser();
mContext.unregisterReceiver(mUserUnlockReceiver);
}
};
@Inject
public FullscreenUserSwitcherViewMediator(
Context context,
@Main Resources resources,
@Main Handler mainHandler,
UserManager userManager,
CarServiceProvider carServiceProvider,
CarTrustAgentUnlockDialogHelper carTrustAgentUnlockDialogHelper,
CarStatusBarKeyguardViewManager carStatusBarKeyguardViewManager,
CarStatusBar carStatusBar,
StatusBarStateController statusBarStateController,
FullScreenUserSwitcherViewController fullScreenUserSwitcherViewController,
ScreenLifecycle screenLifecycle) {
mContext = context;
mIsUserSwitcherEnabled = resources.getBoolean(R.bool.config_enableFullscreenUserSwitcher);
mMainHandler = mainHandler;
mUserManager = userManager;
mCarServiceProvider = carServiceProvider;
mCarServiceProvider.addListener(
car -> mEnrollmentManager = (CarTrustAgentEnrollmentManager) car.getCarManager(
Car.CAR_TRUST_AGENT_ENROLLMENT_SERVICE));
mUnlockDialogHelper = carTrustAgentUnlockDialogHelper;
mCarStatusBarKeyguardViewManager = carStatusBarKeyguardViewManager;
mCarStatusBar = carStatusBar;
mStatusBarStateController = statusBarStateController;
mFullScreenUserSwitcherViewController = fullScreenUserSwitcherViewController;
mScreenLifecycle = screenLifecycle;
mCarUserManagerHelper = new CarUserManagerHelper(mContext);
}
@Override
public void registerListeners() {
registerUserSwitcherShowListeners();
registerUserSwitcherHideListeners();
registerHideKeyguardListeners();
if (mUserManager.isUserUnlocked(UserHandle.USER_SYSTEM)) {
// User0 is unlocked, switched to the initial user
showDialogForInitialUser();
} else {
// listen to USER_UNLOCKED
mContext.registerReceiverAsUser(mUserUnlockReceiver,
UserHandle.getUserHandleForUid(UserHandle.USER_SYSTEM),
new IntentFilter(Intent.ACTION_USER_UNLOCKED),
/* broadcastPermission= */ null,
/* scheduler= */ null);
}
}
private void registerUserSwitcherShowListeners() {
mCarStatusBarKeyguardViewManager.addOnKeyguardCancelClickedListener(this::show);
}
private void registerUserSwitcherHideListeners() {
mStatusBarStateController.addCallback(new StatusBarStateController.StateListener() {
@Override
public void onStateChanged(int newState) {
if (newState == StatusBarState.FULLSCREEN_USER_SWITCHER) {
return;
}
hide();
}
});
}
private void registerHideKeyguardListeners() {
mStatusBarStateController.addCallback(new StatusBarStateController.StateListener() {
@Override
public void onStateChanged(int newState) {
if (newState != StatusBarState.FULLSCREEN_USER_SWITCHER) {
return;
}
dismissKeyguardWhenUserSwitcherNotDisplayed(newState);
}
});
mScreenLifecycle.addObserver(new ScreenLifecycle.Observer() {
@Override
public void onScreenTurnedOn() {
dismissKeyguardWhenUserSwitcherNotDisplayed(mStatusBarStateController.getState());
}
});
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.getAction().equals(Intent.ACTION_USER_SWITCHED)) {
return;
}
// Try to dismiss the keyguard after every user switch.
dismissKeyguardWhenUserSwitcherNotDisplayed(mStatusBarStateController.getState());
}
}, new IntentFilter(Intent.ACTION_USER_SWITCHED));
}
@Override
public void setupOverlayContentViewControllers() {
mFullScreenUserSwitcherViewController.setUserGridSelectionListener(this::onUserSelected);
}
/**
* Every time user clicks on an item in the switcher, if the clicked user has no trusted
* device, we hide the switcher, either gradually or immediately.
* If the user has trusted device, we show an unlock dialog to notify user the unlock
* state.
* When the unlock dialog is dismissed by user, we hide the unlock dialog and the switcher.
* We dismiss the entire keyguard when we hide the switcher if user clicked on the
* foreground user (user we're already logged in as).
*/
private void onUserSelected(UserGridRecyclerView.UserRecord record) {
mSelectedUser = record;
if (record.mInfo != null) {
if (hasScreenLock(record.mInfo.id) && hasTrustedDevice(record.mInfo.id)) {
mUnlockDialogHelper.showUnlockDialog(record.mInfo.id, mOnHideListener);
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "no trusted device enrolled for uid: " + record.mInfo.id);
}
}
dismissUserSwitcher();
}
// We automatically dismiss keyguard unless user switcher is being shown above the keyguard.
private void dismissKeyguardWhenUserSwitcherNotDisplayed(int state) {
if (!mIsUserSwitcherEnabled) {
return; // Not using the full screen user switcher.
}
if (state == StatusBarState.FULLSCREEN_USER_SWITCHER
&& !mFullScreenUserSwitcherViewController.isVisible()) {
// Current execution path continues to set state after this, thus we deffer the
// dismissal to the next execution cycle.
// Dismiss the keyguard if switcher is not visible.
// TODO(b/150402329): Remove once keyguard is implemented using Overlay Window
// abstractions.
mMainHandler.post(mCarStatusBar::dismissKeyguard);
}
}
private boolean hasScreenLock(int uid) {
LockPatternUtils lockPatternUtils = new LockPatternUtils(mContext);
return lockPatternUtils.getCredentialTypeForUser(uid)
!= LockPatternUtils.CREDENTIAL_TYPE_NONE;
}
private boolean hasTrustedDevice(int uid) {
if (mEnrollmentManager == null) { // car service not ready, so it cannot be available.
return false;
}
return !mEnrollmentManager.getEnrolledDeviceInfoForUser(uid).isEmpty();
}
private void dismissUserSwitcher() {
if (mSelectedUser == null) {
Log.e(TAG, "Request to dismiss user switcher, but no user selected");
return;
}
if (mSelectedUser.mType == UserGridRecyclerView.UserRecord.FOREGROUND_USER) {
hide();
mCarStatusBar.dismissKeyguard();
return;
}
hide();
}
private void showDialogForInitialUser() {
int initialUser = mCarUserManagerHelper.getInitialUser();
UserInfo initialUserInfo = mUserManager.getUserInfo(initialUser);
mSelectedUser = new UserGridRecyclerView.UserRecord(initialUserInfo,
UserGridRecyclerView.UserRecord.FOREGROUND_USER);
// If the initial user has screen lock and trusted device, display the unlock dialog on the
// keyguard.
if (hasScreenLock(initialUser) && hasTrustedDevice(initialUser)) {
mUnlockDialogHelper.showUnlockDialogAfterDelay(initialUser,
mOnHideListener);
} else {
// If no trusted device, dismiss the keyguard.
dismissUserSwitcher();
}
}
private void invalidateFullscreenUserSwitcherView() {
mFullScreenUserSwitcherViewController.invalidate();
}
private void hide() {
mFullScreenUserSwitcherViewController.stop();
}
private void show() {
mFullScreenUserSwitcherViewController.start();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 The Android Open Source Project
* 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.car;
package com.android.systemui.car.userswitcher;
import static android.content.DialogInterface.BUTTON_NEGATIVE;
import static android.content.DialogInterface.BUTTON_POSITIVE;
@@ -45,7 +45,6 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
@@ -114,8 +113,6 @@ public class UserGridRecyclerView extends RecyclerView {
/**
* Initializes the adapter that populates the grid layout
*
* @return the adapter
*/
public void buildAdapter() {
List<UserRecord> userRecords = createUserRecords(getUsersForUserGrid());
@@ -236,10 +233,16 @@ public class UserGridRecyclerView extends RecyclerView {
mNewUserName = mRes.getString(R.string.car_new_user);
}
/**
* Clears list of user records.
*/
public void clearUsers() {
mUsers.clear();
}
/**
* Updates list of user records.
*/
public void updateUsers(List<UserRecord> users) {
mUsers = users;
}
@@ -483,6 +486,10 @@ public class UserGridRecyclerView extends RecyclerView {
return mUsers.size();
}
/**
* An extension of {@link RecyclerView.ViewHolder} that also houses the user name and the
* user avatar.
*/
public class UserAdapterViewHolder extends RecyclerView.ViewHolder {
public ImageView mUserAvatarImageView;

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 The Android Open Source Project
* 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.statusbar.car;
package com.android.systemui.car.userswitcher;
import android.annotation.UserIdInt;
import android.content.Context;

View File

@@ -71,6 +71,30 @@ public class CarNavigationBarController {
mShowRight = mContext.getResources().getBoolean(R.bool.config_enableRightNavigationBar);
}
/**
* Hides all navigation bars.
*/
public void hideBars() {
if (mTopView != null) {
mTopView.setVisibility(View.GONE);
}
setBottomWindowVisibility(View.GONE);
setLeftWindowVisibility(View.GONE);
setRightWindowVisibility(View.GONE);
}
/**
* Shows all navigation bars.
*/
public void showBars() {
if (mTopView != null) {
mTopView.setVisibility(View.VISIBLE);
}
setBottomWindowVisibility(View.VISIBLE);
setLeftWindowVisibility(View.VISIBLE);
setRightWindowVisibility(View.VISIBLE);
}
/** Connect to hvac service. */
public void connectToHvac() {
mHvacControllerLazy.get().connectToCarService();

View File

@@ -35,9 +35,9 @@ import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.GridLayoutManager;
import com.android.systemui.R;
import com.android.systemui.car.userswitcher.UserGridRecyclerView;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.QSFooter;
import com.android.systemui.statusbar.car.UserGridRecyclerView;
import java.util.ArrayList;
import java.util.List;

View File

@@ -67,7 +67,6 @@ import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.car.CarDeviceProvisionedController;
import com.android.systemui.car.CarDeviceProvisionedListener;
import com.android.systemui.car.CarServiceProvider;
import com.android.systemui.car.SystemUIPrimaryWindowController;
import com.android.systemui.classifier.FalsingLog;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -95,7 +94,6 @@ import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationViewHierarchyManager;
import com.android.systemui.statusbar.PulseExpansionHandler;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SuperStatusBarViewFactory;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.VibratorHelper;
@@ -181,14 +179,13 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
private Drawable mNotificationPanelBackground;
private final Object mQueueLock = new Object();
private final SystemUIPrimaryWindowController mSystemUIPrimaryWindowController;
private final CarNavigationBarController mCarNavigationBarController;
private final FlingAnimationUtils.Builder mFlingAnimationUtilsBuilder;
private final Lazy<PowerManagerHelper> mPowerManagerHelperLazy;
private final FullscreenUserSwitcher mFullscreenUserSwitcher;
private final ShadeController mShadeController;
private final CarServiceProvider mCarServiceProvider;
private final CarDeviceProvisionedController mCarDeviceProvisionedController;
private final ScreenLifecycle mScreenLifecycle;
private boolean mDeviceIsSetUpForUser = true;
private boolean mIsUserSetupInProgress = false;
@@ -196,7 +193,6 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
private FlingAnimationUtils mFlingAnimationUtils;
private NotificationDataManager mNotificationDataManager;
private NotificationClickHandlerFactory mNotificationClickHandlerFactory;
private ScreenLifecycle mScreenLifecycle;
// The container for the notifications.
private CarNotificationView mNotificationView;
@@ -333,8 +329,6 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
/* Car Settings injected components. */
CarServiceProvider carServiceProvider,
Lazy<PowerManagerHelper> powerManagerHelperLazy,
FullscreenUserSwitcher fullscreenUserSwitcher,
SystemUIPrimaryWindowController systemUIPrimaryWindowController,
CarNavigationBarController carNavigationBarController,
FlingAnimationUtils.Builder flingAnimationUtilsBuilder) {
super(
@@ -423,10 +417,9 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
mShadeController = shadeController;
mCarServiceProvider = carServiceProvider;
mPowerManagerHelperLazy = powerManagerHelperLazy;
mFullscreenUserSwitcher = fullscreenUserSwitcher;
mSystemUIPrimaryWindowController = systemUIPrimaryWindowController;
mCarNavigationBarController = carNavigationBarController;
mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
mScreenLifecycle = screenLifecycle;
}
@Override
@@ -434,18 +427,6 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
mDeviceIsSetUpForUser = mCarDeviceProvisionedController.isCurrentUserSetup();
mIsUserSetupInProgress = mCarDeviceProvisionedController.isCurrentUserSetupInProgress();
// Need to initialize screen lifecycle before calling super.start - before switcher is
// created.
mScreenLifecycle = Dependency.get(ScreenLifecycle.class);
mScreenLifecycle.addObserver(mScreenObserver);
// TODO: Remove the setup of user switcher from Car Status Bar.
mSystemUIPrimaryWindowController.attach();
mFullscreenUserSwitcher.setStatusBar(this);
mFullscreenUserSwitcher.setContainer(
mSystemUIPrimaryWindowController.getBaseLayout().findViewById(
R.id.fullscreen_user_switcher_stub));
// Notification bar related setup.
mInitialBackgroundAlpha = (float) mContext.getResources().getInteger(
R.integer.config_initialNotificationBackgroundAlpha) / 100;
@@ -965,49 +946,6 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
}
}
@Override
public void setLockscreenUser(int newUserId) {
super.setLockscreenUser(newUserId);
// Try to dismiss the keyguard after every user switch.
dismissKeyguardWhenUserSwitcherNotDisplayed();
}
@Override
public void onStateChanged(int newState) {
super.onStateChanged(newState);
if (newState != StatusBarState.FULLSCREEN_USER_SWITCHER) {
mFullscreenUserSwitcher.hide();
} else {
dismissKeyguardWhenUserSwitcherNotDisplayed();
}
}
final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() {
@Override
public void onScreenTurnedOn() {
dismissKeyguardWhenUserSwitcherNotDisplayed();
}
};
// We automatically dismiss keyguard unless user switcher is being shown on the keyguard.
private void dismissKeyguardWhenUserSwitcherNotDisplayed() {
if (!mUserSwitcherController.useFullscreenUserSwitcher()) {
return; // Not using the full screen user switcher.
}
if (mState == StatusBarState.FULLSCREEN_USER_SWITCHER
&& !mFullscreenUserSwitcher.isVisible()) {
// Current execution path continues to set state after this, thus we deffer the
// dismissal to the next execution cycle.
postDismissKeyguard(); // Dismiss the keyguard if switcher is not visible.
}
}
public void postDismissKeyguard() {
mHandler.post(this::dismissKeyguard);
}
/**
* Dismisses the keyguard and shows bouncer if authentication is necessary.
*/

View File

@@ -33,6 +33,9 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -42,7 +45,7 @@ public class CarStatusBarKeyguardViewManager extends StatusBarKeyguardViewManage
protected boolean mShouldHideNavBar;
private final CarNavigationBarController mCarNavigationBarController;
private final FullscreenUserSwitcher mFullscreenUserSwitcher;
private Set<OnKeyguardCancelClickedListener> mKeygaurdCancelClickedListenerSet;
@Inject
public CarStatusBarKeyguardViewManager(Context context,
@@ -56,8 +59,7 @@ public class CarStatusBarKeyguardViewManager extends StatusBarKeyguardViewManage
NotificationShadeWindowController notificationShadeWindowController,
KeyguardStateController keyguardStateController,
NotificationMediaManager notificationMediaManager,
CarNavigationBarController carNavigationBarController,
FullscreenUserSwitcher fullscreenUserSwitcher) {
CarNavigationBarController carNavigationBarController) {
super(context, callback, lockPatternUtils, sysuiStatusBarStateController,
configurationController, keyguardUpdateMonitor, navigationModeController,
dockManager, notificationShadeWindowController, keyguardStateController,
@@ -65,7 +67,7 @@ public class CarStatusBarKeyguardViewManager extends StatusBarKeyguardViewManage
mShouldHideNavBar = context.getResources()
.getBoolean(R.bool.config_hideNavWhenKeyguardBouncerShown);
mCarNavigationBarController = carNavigationBarController;
mFullscreenUserSwitcher = fullscreenUserSwitcher;
mKeygaurdCancelClickedListenerSet = new HashSet<>();
}
@Override
@@ -95,7 +97,7 @@ public class CarStatusBarKeyguardViewManager extends StatusBarKeyguardViewManage
*/
@Override
public void onCancelClicked() {
mFullscreenUserSwitcher.show();
mKeygaurdCancelClickedListenerSet.forEach(OnKeyguardCancelClickedListener::onCancelClicked);
}
/**
@@ -105,4 +107,31 @@ public class CarStatusBarKeyguardViewManager extends StatusBarKeyguardViewManage
*/
@Override
public void onDensityOrFontScaleChanged() { }
/**
* Add listener for keyguard cancel clicked.
*/
public void addOnKeyguardCancelClickedListener(
OnKeyguardCancelClickedListener keyguardCancelClickedListener) {
mKeygaurdCancelClickedListenerSet.add(keyguardCancelClickedListener);
}
/**
* Remove listener for keyguard cancel clicked.
*/
public void removeOnKeyguardCancelClickedListener(
OnKeyguardCancelClickedListener keyguardCancelClickedListener) {
mKeygaurdCancelClickedListenerSet.remove(keyguardCancelClickedListener);
}
/**
* Defines a callback for keyguard cancel button clicked listeners.
*/
public interface OnKeyguardCancelClickedListener {
/**
* Called when keyguard cancel button is clicked.
*/
void onCancelClicked();
}
}

View File

@@ -31,7 +31,6 @@ import com.android.systemui.assist.AssistManager;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.car.CarServiceProvider;
import com.android.systemui.car.SystemUIPrimaryWindowController;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.keyguard.DismissCallbackRegistry;
@@ -205,8 +204,6 @@ public class CarStatusBarModule {
StatusBarTouchableRegionManager statusBarTouchableRegionManager,
CarServiceProvider carServiceProvider,
Lazy<PowerManagerHelper> powerManagerHelperLazy,
FullscreenUserSwitcher fullscreenUserSwitcher,
SystemUIPrimaryWindowController systemUIPrimaryWindowController,
CarNavigationBarController carNavigationBarController,
FlingAnimationUtils.Builder flingAnimationUtilsBuilder) {
return new CarStatusBar(
@@ -288,8 +285,6 @@ public class CarStatusBarModule {
statusBarTouchableRegionManager,
carServiceProvider,
powerManagerHelperLazy,
fullscreenUserSwitcher,
systemUIPrimaryWindowController,
carNavigationBarController,
flingAnimationUtilsBuilder);
}

View File

@@ -1,271 +0,0 @@
/*
* Copyright (C) 2018 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.car;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.car.Car;
import android.car.trust.CarTrustAgentEnrollmentManager;
import android.car.userlib.CarUserManagerHelper;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.view.View;
import android.view.ViewStub;
import androidx.recyclerview.widget.GridLayoutManager;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import com.android.systemui.car.CarServiceProvider;
import com.android.systemui.car.SystemUIPrimaryWindowController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.statusbar.car.CarTrustAgentUnlockDialogHelper.OnHideListener;
import com.android.systemui.statusbar.car.UserGridRecyclerView.UserRecord;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Manages the fullscreen user switcher.
*/
@Singleton
public class FullscreenUserSwitcher {
private static final String TAG = FullscreenUserSwitcher.class.getSimpleName();
private final Context mContext;
private final Resources mResources;
private final UserManager mUserManager;
private final CarServiceProvider mCarServiceProvider;
private final CarTrustAgentUnlockDialogHelper mUnlockDialogHelper;
private final SystemUIPrimaryWindowController mSystemUIPrimaryWindowController;
private CarStatusBar mCarStatusBar;
private final int mShortAnimDuration;
private View mParent;
private UserGridRecyclerView mUserGridView;
private CarTrustAgentEnrollmentManager mEnrollmentManager;
private UserGridRecyclerView.UserRecord mSelectedUser;
private CarUserManagerHelper mCarUserManagerHelper;
private final BroadcastReceiver mUserUnlockReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "user 0 is unlocked, SharedPreference is accessible.");
}
showDialogForInitialUser();
mContext.unregisterReceiver(mUserUnlockReceiver);
}
};
@Inject
public FullscreenUserSwitcher(
Context context,
@Main Resources resources,
UserManager userManager,
CarServiceProvider carServiceProvider,
CarTrustAgentUnlockDialogHelper carTrustAgentUnlockDialogHelper,
SystemUIPrimaryWindowController systemUIPrimaryWindowController) {
mContext = context;
mResources = resources;
mUserManager = userManager;
mCarServiceProvider = carServiceProvider;
mUnlockDialogHelper = carTrustAgentUnlockDialogHelper;
mSystemUIPrimaryWindowController = systemUIPrimaryWindowController;
mShortAnimDuration = mResources.getInteger(android.R.integer.config_shortAnimTime);
}
/** Sets the status bar which gives an entry point to dismiss the keyguard. */
// TODO: Remove this in favor of a keyguard controller.
public void setStatusBar(CarStatusBar statusBar) {
mCarStatusBar = statusBar;
}
/** Returns {@code true} if the user switcher already has a parent view. */
public boolean isAttached() {
return mParent != null;
}
/** Sets the {@link ViewStub} to show the user switcher. */
public void setContainer(ViewStub containerStub) {
if (isAttached()) {
return;
}
mParent = containerStub.inflate();
View container = mParent.findViewById(R.id.container);
// Initialize user grid.
mUserGridView = container.findViewById(R.id.user_grid);
GridLayoutManager layoutManager = new GridLayoutManager(mContext,
mResources.getInteger(R.integer.user_fullscreen_switcher_num_col));
mUserGridView.setLayoutManager(layoutManager);
mUserGridView.buildAdapter();
mUserGridView.setUserSelectionListener(this::onUserSelected);
mCarUserManagerHelper = new CarUserManagerHelper(mContext);
mCarServiceProvider.addListener(
car -> mEnrollmentManager = (CarTrustAgentEnrollmentManager) car.getCarManager(
Car.CAR_TRUST_AGENT_ENROLLMENT_SERVICE));
IntentFilter filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
if (mUserManager.isUserUnlocked(UserHandle.USER_SYSTEM)) {
// User0 is unlocked, switched to the initial user
showDialogForInitialUser();
} else {
// listen to USER_UNLOCKED
mContext.registerReceiverAsUser(mUserUnlockReceiver,
UserHandle.getUserHandleForUid(UserHandle.USER_SYSTEM),
filter,
/* broadcastPermission= */ null,
/* scheduler */ null);
}
}
private void showDialogForInitialUser() {
int initialUser = mCarUserManagerHelper.getInitialUser();
UserInfo initialUserInfo = mUserManager.getUserInfo(initialUser);
mSelectedUser = new UserRecord(initialUserInfo, UserRecord.FOREGROUND_USER);
// If the initial user has screen lock and trusted device, display the unlock dialog on the
// keyguard.
if (hasScreenLock(initialUser) && hasTrustedDevice(initialUser)) {
mUnlockDialogHelper.showUnlockDialogAfterDelay(initialUser,
mOnHideListener);
} else {
// If no trusted device, dismiss the keyguard.
dismissUserSwitcher();
}
}
/**
* Makes user grid visible.
*/
public void show() {
if (!isAttached()) {
return;
}
mParent.setVisibility(View.VISIBLE);
mSystemUIPrimaryWindowController.setWindowExpanded(true);
}
/**
* Hides the user grid.
*/
public void hide() {
if (!isAttached()) {
return;
}
mParent.setVisibility(View.INVISIBLE);
mSystemUIPrimaryWindowController.setWindowExpanded(false);
}
/**
* @return {@code true} if user grid is visible, {@code false} otherwise.
*/
public boolean isVisible() {
if (!isAttached()) {
return false;
}
return mParent.getVisibility() == View.VISIBLE;
}
/**
* Every time user clicks on an item in the switcher, if the clicked user has no trusted device,
* we hide the switcher, either gradually or immediately.
*
* If the user has trusted device, we show an unlock dialog to notify user the unlock state.
* When the unlock dialog is dismissed by user, we hide the unlock dialog and the switcher.
*
* We dismiss the entire keyguard when we hide the switcher if user clicked on the foreground
* user (user we're already logged in as).
*/
private void onUserSelected(UserGridRecyclerView.UserRecord record) {
mSelectedUser = record;
if (record.mInfo != null) {
if (hasScreenLock(record.mInfo.id) && hasTrustedDevice(record.mInfo.id)) {
mUnlockDialogHelper.showUnlockDialog(record.mInfo.id, mOnHideListener);
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "no trusted device enrolled for uid: " + record.mInfo.id);
}
}
dismissUserSwitcher();
}
private void dismissUserSwitcher() {
if (mSelectedUser == null) {
Log.e(TAG, "Request to dismiss user switcher, but no user selected");
return;
}
if (mSelectedUser.mType == UserRecord.FOREGROUND_USER) {
hide();
mCarStatusBar.dismissKeyguard();
return;
}
// Switching is about to happen, since it takes time, fade out the switcher gradually.
fadeOut();
}
private void fadeOut() {
mUserGridView.animate()
.alpha(0.0f)
.setDuration(mShortAnimDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
hide();
mUserGridView.setAlpha(1.0f);
}
});
}
private boolean hasScreenLock(int uid) {
LockPatternUtils lockPatternUtils = new LockPatternUtils(mContext);
return lockPatternUtils.getCredentialTypeForUser(uid)
!= LockPatternUtils.CREDENTIAL_TYPE_NONE;
}
private boolean hasTrustedDevice(int uid) {
if (mEnrollmentManager == null) { // car service not ready, so it cannot be available.
return false;
}
return !mEnrollmentManager.getEnrolledDeviceInfoForUser(uid).isEmpty();
}
private OnHideListener mOnHideListener = new OnHideListener() {
@Override
public void onHide(boolean dismissUserSwitcher) {
if (dismissUserSwitcher) {
dismissUserSwitcher();
} else {
// Re-draw the parent view, otherwise the unlock dialog will not be removed from
// the screen immediately.
mParent.invalidate();
}
}
};
}

View File

@@ -0,0 +1,123 @@
/*
* 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.window;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
/**
* Owns a {@link View} that is present in SystemUIOverlayWindow.
*/
public class OverlayViewController {
private final int mStubId;
private final OverlayViewGlobalStateController mOverlayViewGlobalStateController;
private View mLayout;
public OverlayViewController(int stubId,
OverlayViewGlobalStateController overlayViewGlobalStateController) {
mLayout = null;
mStubId = stubId;
mOverlayViewGlobalStateController = overlayViewGlobalStateController;
}
/**
* Shows content of {@link OverlayViewController}.
*
* Should be used to show view externally and in particular by {@link OverlayViewMediator}.
*/
public final void start() {
mOverlayViewGlobalStateController.showView(/* viewController= */ this, this::show);
}
/**
* Hides content of {@link OverlayViewController}.
*
* Should be used to hide view externally and in particular by {@link OverlayViewMediator}.
*/
public final void stop() {
mOverlayViewGlobalStateController.hideView(/* viewController= */ this, this::hide);
}
/**
* Inflate layout owned by controller.
*/
public final void inflate(ViewGroup baseLayout) {
ViewStub viewStub = baseLayout.findViewById(mStubId);
mLayout = viewStub.inflate();
onFinishInflate();
}
/**
* Called once inflate finishes.
*/
protected void onFinishInflate() {
// no-op
}
/**
* Returns [@code true} if layout owned by controller has been inflated.
*/
public final boolean isInflated() {
return mLayout != null;
}
private void show() {
if (mLayout == null) {
// layout must be inflated before show() is called.
return;
}
showInternal();
}
/**
* Subclasses should override this method to implement reveal animations and implement logic
* specific to when the layout owned by the controller is shown.
*
* Should only be overridden by Superclass but not called by any {@link OverlayViewMediator}.
*/
protected void showInternal() {
mLayout.setVisibility(View.VISIBLE);
}
private void hide() {
if (mLayout == null) {
// layout must be inflated before hide() is called.
return;
}
hideInternal();
}
/**
* Subclasses should override this method to implement conceal animations and implement logic
* specific to when the layout owned by the controller is hidden.
*
* Should only be overridden by Superclass but not called by any {@link OverlayViewMediator}.
*/
protected void hideInternal() {
mLayout.setVisibility(View.GONE);
}
/**
* Provides access to layout owned by controller.
*/
protected final View getLayout() {
return mLayout;
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.window;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.navigationbar.car.CarNavigationBarController;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* This controller is responsible for the following:
* <p><ul>
* <li>Holds the global state for SystemUIOverlayWindow.
* <li>Allows {@link SystemUIOverlayWindowManager} to register {@link OverlayViewMediator}(s).
* <li>Enables {@link OverlayViewController)(s) to reveal/conceal themselves while respecting the
* global state of SystemUIOverlayWindow.
* </ul>
*/
@Singleton
public class OverlayViewGlobalStateController {
private static final String TAG = OverlayViewGlobalStateController.class.getSimpleName();
private final SystemUIOverlayWindowController mSystemUIOverlayWindowController;
private final CarNavigationBarController mCarNavigationBarController;
@VisibleForTesting
Set<String> mShownSet;
@Inject
public OverlayViewGlobalStateController(
CarNavigationBarController carNavigationBarController,
SystemUIOverlayWindowController systemUIOverlayWindowController) {
mSystemUIOverlayWindowController = systemUIOverlayWindowController;
mSystemUIOverlayWindowController.attach();
mCarNavigationBarController = carNavigationBarController;
mShownSet = new HashSet<>();
}
/**
* Register {@link OverlayViewMediator} to use in SystemUIOverlayWindow.
*/
public void registerMediator(OverlayViewMediator overlayViewMediator) {
Log.d(TAG, "Registering content mediator: " + overlayViewMediator.getClass().getName());
overlayViewMediator.registerListeners();
overlayViewMediator.setupOverlayContentViewControllers();
}
/**
* Show content in Overlay Window.
*/
public void showView(OverlayViewController viewController, Runnable show) {
if (mShownSet.isEmpty()) {
mCarNavigationBarController.hideBars();
mSystemUIOverlayWindowController.setWindowExpanded(true);
}
if (!viewController.isInflated()) {
viewController.inflate(mSystemUIOverlayWindowController.getBaseLayout());
}
show.run();
mShownSet.add(viewController.getClass().getName());
Log.d(TAG, "Content shown: " + viewController.getClass().getName());
}
/**
* Hide content in Overlay Window.
*/
public void hideView(OverlayViewController viewController, Runnable hide) {
if (!viewController.isInflated()) {
Log.d(TAG, "Content cannot be hidden since it isn't inflated: "
+ viewController.getClass().getName());
return;
}
if (!mShownSet.contains(viewController.getClass().getName())) {
Log.d(TAG, "Content cannot be hidden since it isn't shown: "
+ viewController.getClass().getName());
return;
}
hide.run();
mShownSet.remove(viewController.getClass().getName());
if (mShownSet.isEmpty()) {
mCarNavigationBarController.showBars();
mSystemUIOverlayWindowController.setWindowExpanded(false);
}
Log.d(TAG, "Content hidden: " + viewController.getClass().getName());
}
}

View File

@@ -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.window;
/**
* Controls when to show and hide {@link OverlayViewController}(s).
*/
public interface OverlayViewMediator {
/**
* Register listeners that could use ContentVisibilityAdjuster to show/hide content.
*/
void registerListeners();
/**
* Allows for post-inflation callbacks and listeners to be set inside required {@link
* OverlayViewController}(s).
*/
void setupOverlayContentViewControllers();
}

View File

@@ -0,0 +1,37 @@
/*
* 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.window;
import com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
/**
* Dagger injection module for {@link SystemUIOverlayWindowManager}
*/
@Module
public abstract class OverlayWindowModule {
/** Inject into FullscreenUserSwitcherViewsMediator. */
@Binds
@IntoMap
@ClassKey(FullscreenUserSwitcherViewMediator.class)
public abstract OverlayViewMediator bindFullscreenUserSwitcherViewsMediator(
FullscreenUserSwitcherViewMediator overlayViewsMediator);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.car;
package com.android.systemui.window;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
@@ -41,7 +41,7 @@ import javax.inject.Singleton;
* this window for the notification panel.
*/
@Singleton
public class SystemUIPrimaryWindowController implements
public class SystemUIOverlayWindowController implements
ConfigurationController.ConfigurationListener {
private final Context mContext;
@@ -57,7 +57,7 @@ public class SystemUIPrimaryWindowController implements
private boolean mIsAttached = false;
@Inject
public SystemUIPrimaryWindowController(
public SystemUIOverlayWindowController(
Context context,
@Main Resources resources,
WindowManager windowManager,
@@ -77,7 +77,7 @@ public class SystemUIPrimaryWindowController implements
mLpChanged = new WindowManager.LayoutParams();
mBaseLayout = (ViewGroup) LayoutInflater.from(context)
.inflate(R.layout.sysui_primary_window, /* root= */ null, false);
.inflate(R.layout.sysui_overlay_window, /* root= */ null, false);
configurationController.addCallback(this);
}
@@ -115,7 +115,7 @@ public class SystemUIPrimaryWindowController implements
mLp.gravity = Gravity.TOP;
mLp.setFitInsetsTypes(/* types= */ 0);
mLp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
mLp.setTitle("SystemUIPrimaryWindow");
mLp.setTitle("SystemUIOverlayWindow");
mLp.packageName = mContext.getPackageName();
mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;

View File

@@ -0,0 +1,92 @@
/*
* 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.window;
import android.content.Context;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
/**
* Registers {@link OverlayViewMediator}(s) and synchronizes their calls to hide/show {@link
* OverlayViewController}(s) to allow for the correct visibility of system bars.
*/
@Singleton
public class SystemUIOverlayWindowManager extends SystemUI {
private static final String TAG = "SystemUIOverlayWindowManager";
private final Map<Class<?>, Provider<OverlayViewMediator>>
mContentMediatorCreators;
private final OverlayViewGlobalStateController mOverlayViewGlobalStateController;
@Inject
public SystemUIOverlayWindowManager(
Context context,
Map<Class<?>, Provider<OverlayViewMediator>> contentMediatorCreators,
OverlayViewGlobalStateController overlayViewGlobalStateController) {
super(context);
mContentMediatorCreators = contentMediatorCreators;
mOverlayViewGlobalStateController = overlayViewGlobalStateController;
}
@Override
public void start() {
String[] names = mContext.getResources().getStringArray(
R.array.config_carSystemUIOverlayViewsMediators);
startServices(names);
}
private void startServices(String[] services) {
for (String clsName : services) {
try {
OverlayViewMediator obj = resolveContentMediator(clsName);
if (obj == null) {
Constructor constructor = Class.forName(clsName).getConstructor(Context.class);
obj = (OverlayViewMediator) constructor.newInstance(this);
}
mOverlayViewGlobalStateController.registerMediator(obj);
} catch (ClassNotFoundException
| NoSuchMethodException
| IllegalAccessException
| InstantiationException
| InvocationTargetException ex) {
throw new RuntimeException(ex);
}
}
}
private OverlayViewMediator resolveContentMediator(String className) {
return resolve(className, mContentMediatorCreators);
}
private <T> T resolve(String className, Map<Class<?>, Provider<T>> creators) {
try {
Class<?> clazz = Class.forName(className);
Provider<T> provider = creators.get(clazz);
return provider == null ? null : provider.get();
} catch (ClassNotFoundException e) {
return null;
}
}
}

View File

@@ -0,0 +1,22 @@
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/overlay_view_controller_test">
</LinearLayout>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<!-- Fullscreen views in sysui should be listed here in increasing Z order. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@android:color/transparent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub android:id="@+id/overlay_view_controller_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/overlay_view_controller_stub"/>
</FrameLayout>

View File

@@ -0,0 +1,158 @@
/*
* 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.window;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.tests.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@SmallTest
public class OverlayViewControllerTest extends SysuiTestCase {
private MockOverlayViewController mOverlayViewController;
private ViewGroup mBaseLayout;
@Mock
private OverlayViewGlobalStateController mOverlayViewGlobalStateController;
@Captor
private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(/* testClass= */ this);
mOverlayViewController = new MockOverlayViewController(R.id.overlay_view_controller_stub,
mOverlayViewGlobalStateController);
mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate(
R.layout.overlay_view_controller_test, /* root= */ null);
}
@Test
public void inflate_layoutInitialized() {
mOverlayViewController.inflate(mBaseLayout);
assertThat(mOverlayViewController.getLayout().getId()).isEqualTo(
R.id.overlay_view_controller_test);
}
@Test
public void inflate_onFinishInflateCalled() {
mOverlayViewController.inflate(mBaseLayout);
assertThat(mOverlayViewController.mOnFinishInflateCalled).isTrue();
}
@Test
public void start_viewInflated_viewShown() {
mOverlayViewController.inflate(mBaseLayout);
mOverlayViewController.start();
verify(mOverlayViewGlobalStateController).showView(eq(mOverlayViewController),
mRunnableArgumentCaptor.capture());
mRunnableArgumentCaptor.getValue().run();
assertThat(mOverlayViewController.mShowInternalCalled).isTrue();
}
@Test
public void stop_viewInflated_viewHidden() {
mOverlayViewController.inflate(mBaseLayout);
mOverlayViewController.stop();
verify(mOverlayViewGlobalStateController).hideView(eq(mOverlayViewController),
mRunnableArgumentCaptor.capture());
mRunnableArgumentCaptor.getValue().run();
assertThat(mOverlayViewController.mHideInternalCalled).isTrue();
}
@Test
public void start_viewNotInflated_viewNotShown() {
mOverlayViewController.start();
verify(mOverlayViewGlobalStateController).showView(eq(mOverlayViewController),
mRunnableArgumentCaptor.capture());
mRunnableArgumentCaptor.getValue().run();
assertThat(mOverlayViewController.mShowInternalCalled).isFalse();
}
@Test
public void stop_viewNotInflated_viewNotHidden() {
mOverlayViewController.stop();
verify(mOverlayViewGlobalStateController).hideView(eq(mOverlayViewController),
mRunnableArgumentCaptor.capture());
mRunnableArgumentCaptor.getValue().run();
assertThat(mOverlayViewController.mHideInternalCalled).isFalse();
}
private static class MockOverlayViewController extends OverlayViewController {
boolean mOnFinishInflateCalled = false;
boolean mShowInternalCalled = false;
boolean mHideInternalCalled = false;
MockOverlayViewController(int stubId,
OverlayViewGlobalStateController overlayViewGlobalStateController) {
super(stubId, overlayViewGlobalStateController);
}
@Override
protected void onFinishInflate() {
mOnFinishInflateCalled = true;
}
@Override
protected void showInternal() {
mShowInternalCalled = true;
}
@Override
protected void hideInternal() {
mHideInternalCalled = true;
}
}
}

View File

@@ -0,0 +1,250 @@
/*
* 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.window;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.navigationbar.car.CarNavigationBarController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@SmallTest
public class OverlayViewGlobalStateControllerTest extends SysuiTestCase {
private static final String MOCK_OVERLAY_VIEW_CONTROLLER_NAME = "OverlayViewController";
private OverlayViewGlobalStateController mOverlayViewGlobalStateController;
private ViewGroup mBaseLayout;
@Mock
private CarNavigationBarController mCarNavigationBarController;
@Mock
private SystemUIOverlayWindowController mSystemUIOverlayWindowController;
@Mock
private OverlayViewMediator mOverlayViewMediator;
@Mock
private OverlayViewController mOverlayViewController;
@Mock
private Runnable mRunnable;
@Before
public void setUp() {
MockitoAnnotations.initMocks(/* testClass= */ this);
mOverlayViewGlobalStateController = new OverlayViewGlobalStateController(
mCarNavigationBarController, mSystemUIOverlayWindowController);
verify(mSystemUIOverlayWindowController).attach();
mBaseLayout = new FrameLayout(mContext);
when(mSystemUIOverlayWindowController.getBaseLayout()).thenReturn(mBaseLayout);
}
@Test
public void registerMediator_overlayViewMediatorListenersRegistered() {
mOverlayViewGlobalStateController.registerMediator(mOverlayViewMediator);
verify(mOverlayViewMediator).registerListeners();
}
@Test
public void registerMediator_overlayViewMediatorViewControllerSetup() {
mOverlayViewGlobalStateController.registerMediator(mOverlayViewMediator);
verify(mOverlayViewMediator).setupOverlayContentViewControllers();
}
@Test
public void showView_nothingAlreadyShown_navigationBarsHidden() {
mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable);
verify(mCarNavigationBarController).hideBars();
}
@Test
public void showView_nothingAlreadyShown_windowIsExpanded() {
mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable);
verify(mSystemUIOverlayWindowController).setWindowExpanded(true);
}
@Test
public void showView_somethingAlreadyShown_navigationBarsHidden() {
mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME);
mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable);
verify(mCarNavigationBarController, never()).hideBars();
}
@Test
public void showView_somethingAlreadyShown_windowIsExpanded() {
mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME);
mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable);
verify(mSystemUIOverlayWindowController, never()).setWindowExpanded(true);
}
@Test
public void showView_viewControllerNotInflated_inflateViewController() {
when(mOverlayViewController.isInflated()).thenReturn(false);
mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable);
verify(mOverlayViewController).inflate(mBaseLayout);
}
@Test
public void showView_viewControllerInflated_inflateViewControllerNotCalled() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable);
verify(mOverlayViewController, never()).inflate(mBaseLayout);
}
@Test
public void showView_showRunnableCalled() {
mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable);
verify(mRunnable).run();
}
@Test
public void showView_overlayViewControllerAddedToShownSet() {
mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable);
assertThat(mOverlayViewGlobalStateController.mShownSet.contains(
mOverlayViewController.getClass().getName())).isTrue();
}
@Test
public void hideView_viewControllerNotInflated_hideRunnableNotCalled() {
when(mOverlayViewController.isInflated()).thenReturn(false);
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
verify(mRunnable, never()).run();
}
@Test
public void hideView_nothingShown_hideRunnableNotCalled() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.mShownSet.clear();
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
verify(mRunnable, never()).run();
}
@Test
public void hideView_viewControllerNotShown_hideRunnableNotCalled() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME);
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
verify(mRunnable, never()).run();
}
@Test
public void hideView_viewControllerShown_hideRunnableCalled() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.mShownSet.add(
mOverlayViewController.getClass().getName());
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
verify(mRunnable).run();
}
@Test
public void hideView_viewControllerOnlyShown_nothingShown() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.mShownSet.add(
mOverlayViewController.getClass().getName());
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
assertThat(mOverlayViewGlobalStateController.mShownSet.isEmpty()).isTrue();
}
@Test
public void hideView_viewControllerNotOnlyShown_navigationBarNotShown() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.mShownSet.add(
mOverlayViewController.getClass().getName());
mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME);
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
verify(mCarNavigationBarController, never()).showBars();
}
@Test
public void hideView_viewControllerNotOnlyShown_windowNotCollapsed() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.mShownSet.add(
mOverlayViewController.getClass().getName());
mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME);
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
verify(mSystemUIOverlayWindowController, never()).setWindowExpanded(false);
}
@Test
public void hideView_viewControllerOnlyShown_navigationBarShown() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.mShownSet.add(
mOverlayViewController.getClass().getName());
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
verify(mCarNavigationBarController).showBars();
}
@Test
public void hideView_viewControllerOnlyShown_windowCollapsed() {
when(mOverlayViewController.isInflated()).thenReturn(true);
mOverlayViewGlobalStateController.mShownSet.add(
mOverlayViewController.getClass().getName());
mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable);
verify(mSystemUIOverlayWindowController).setWindowExpanded(false);
}
}