Wire the new FooterActions implementation behind a flag

This CL wires the new footer actions implementation from ag/19678215
behind a new SysUI flag. This flag will be enabled by default in a
follow-up CL.

Bug: 242040009
Test: atest FooterActionsViewModelTest
Test: atest FooterActionsScreenshotTest
Change-Id: If23711e400a86ad247666a1532cb17616c01ed85
This commit is contained in:
Jordan Demeulenaere
2022-08-23 15:35:59 +02:00
parent 6fb22d4b16
commit 4327ce40f8
24 changed files with 728 additions and 69 deletions

View File

@@ -6,4 +6,12 @@ packages/SystemUI/src/com/android/systemui/keyguard/data
packages/SystemUI/src/com/android/systemui/keyguard/dagger
packages/SystemUI/src/com/android/systemui/keyguard/domain
packages/SystemUI/src/com/android/systemui/keyguard/shared
packages/SystemUI/src/com/android/systemui/keyguard/ui
packages/SystemUI/src/com/android/systemui/keyguard/ui
packages/SystemUI/src/com/android/systemui/qs/footer
packages/SystemUI/src/com/android/systemui/security
packages/SystemUI/src/com/android/systemui/common/
packages/SystemUI/tests/utils/src/com/android/systemui/qs/
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeSecurityController.kt
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeUserInfoController.kt
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/MockUserSwitcherControllerWrapper.kt
packages/SystemUI/tests/src/com/android/systemui/qs/footer/

View File

@@ -14,6 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- TODO(b/242040009): Remove this file. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"

View File

@@ -16,6 +16,7 @@
-->
<!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc -->
<!-- TODO(b/242040009): Clean up this file. -->
<com.android.systemui.qs.FooterActionsView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
@@ -25,7 +26,7 @@
android:paddingTop="8dp"
android:paddingBottom="@dimen/qs_footer_actions_bottom_padding"
android:background="@drawable/qs_footer_actions_background"
android:gravity="center_vertical"
android:gravity="center_vertical|end"
android:layout_gravity="bottom"
>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2022 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.statusbar.AlphaOptimizedFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/qs_footer_action_button_size"
android:layout_height="@dimen/qs_footer_action_button_size"
android:visibility="gone">
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/qs_footer_icon_size"
android:layout_height="@dimen/qs_footer_icon_size"
android:layout_gravity="center"
android:scaleType="centerInside" />
</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2022 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/qs_footer_action_button_size"
android:layout_height="@dimen/qs_footer_action_button_size"
android:background="@drawable/qs_footer_action_circle"
android:visibility="gone">
<TextView
android:id="@+id/number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
android:layout_gravity="center"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"/>
<ImageView
android:id="@+id/new_dot"
android:layout_width="12dp"
android:layout_height="12dp"
android:scaleType="fitCenter"
android:layout_gravity="bottom|end"
android:src="@drawable/fgs_dot"
android:contentDescription="@string/fgs_dot_content_description" />
</FrameLayout>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2022 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="0dp"
android:layout_height="@dimen/qs_security_footer_single_line_height"
android:layout_weight="1"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/qs_footer_padding"
android:gravity="center_vertical"
android:layout_marginEnd="@dimen/qs_footer_action_inset"
android:background="@drawable/qs_security_footer_background"
android:visibility="gone">
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/qs_footer_icon_size"
android:layout_height="@dimen/qs_footer_icon_size"
android:gravity="start"
android:layout_marginEnd="12dp"
android:contentDescription="@null"
android:src="@drawable/ic_info_outline"
android:tint="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="1"
android:ellipsize="end"
android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
android:textColor="?android:attr/textColorSecondary"/>
<ImageView
android:id="@+id/new_dot"
android:layout_width="12dp"
android:layout_height="12dp"
android:scaleType="fitCenter"
android:src="@drawable/fgs_dot"
android:contentDescription="@string/fgs_dot_content_description"
/>
<ImageView
android:id="@+id/chevron_icon"
android:layout_width="@dimen/qs_footer_icon_size"
android:layout_height="@dimen/qs_footer_icon_size"
android:layout_marginStart="8dp"
android:contentDescription="@null"
android:src="@*android:drawable/ic_chevron_end"
android:autoMirrored="true"
android:tint="?android:attr/textColorSecondary" />
</LinearLayout>

View File

@@ -14,6 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- TODO(b/242040009): Remove this file. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2022 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.common.ui
import android.view.View
import com.android.systemui.common.shared.model.ContentDescription
object ContentDescriptionViewBinder {
fun bind(
contentDescription: ContentDescription,
view: View,
) {
when (contentDescription) {
is ContentDescription.Loaded -> view.contentDescription = contentDescription.description
is ContentDescription.Resource -> {
view.contentDescription = view.context.resources.getString(contentDescription.res)
}
}
}
}

View File

@@ -48,8 +48,12 @@ import com.android.systemui.navigationbar.NavigationBarComponent;
import com.android.systemui.people.PeopleModule;
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
import com.android.systemui.privacy.PrivacyModule;
import com.android.systemui.qs.FgsManagerController;
import com.android.systemui.qs.FgsManagerControllerImpl;
import com.android.systemui.qs.footer.dagger.FooterActionsModule;
import com.android.systemui.recents.Recents;
import com.android.systemui.screenshot.dagger.ScreenshotModule;
import com.android.systemui.security.data.repository.SecurityRepositoryModule;
import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.smartspace.dagger.SmartspaceModule;
@@ -121,6 +125,7 @@ import dagger.Provides;
DemoModeModule.class,
FalsingModule.class,
FlagsModule.class,
FooterActionsModule.class,
LogModule.class,
MediaProjectionModule.class,
PeopleHubModule.class,
@@ -131,6 +136,7 @@ import dagger.Provides;
ScreenshotModule.class,
SensorModule.class,
MultiUserUtilsModule.class,
SecurityRepositoryModule.class,
SettingsUtilModule.class,
SmartRepliesInflationModule.class,
SmartspaceModule.class,
@@ -256,4 +262,7 @@ public abstract class SystemUIModule {
return Optional.empty();
}
}
@Binds
abstract FgsManagerController bindFgsManagerController(FgsManagerControllerImpl impl);
}

View File

@@ -147,6 +147,8 @@ public class Flags {
public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
public static final UnreleasedFlag NEW_FOOTER_ACTIONS = new UnreleasedFlag(507);
/***************************************/
// 600- status bar
public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =

View File

@@ -56,6 +56,7 @@ import javax.inject.Provider
* determined by [buttonsVisibleState]
*/
@QSScope
// TODO(b/242040009): Remove this file.
internal class FooterActionsController @Inject constructor(
view: FooterActionsView,
multiUserSwitchControllerFactory: MultiUserSwitchController.Factory,

View File

@@ -38,6 +38,7 @@ import com.android.systemui.statusbar.phone.MultiUserSwitch
* in split shade mode visible also in collapsed state. May contain up to 5 buttons: settings,
* edit tiles, power off and conditionally: user switch and tuner
*/
// TODO(b/242040009): Remove this file.
class FooterActionsView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {
private lateinit var settingsContainer: View
private lateinit var multiUserSwitch: MultiUserSwitch

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 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.qs
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
/** Controller for the footer actions. This manages the initialization of its dependencies. */
@SysUISingleton
class NewFooterActionsController
@Inject
// TODO(b/242040009): Rename this to FooterActionsController.
constructor(
private val fgsManagerController: FgsManagerController,
) {
fun init() {
fgsManagerController.init()
}
}

View File

@@ -41,6 +41,7 @@ import javax.inject.Named;
/**
* Footer entry point for the foreground service manager
*/
// TODO(b/242040009): Remove this file.
@QSScope
public class QSFgsManagerFooter implements View.OnClickListener,
FgsManagerController.OnDialogDismissedListener,

View File

@@ -36,6 +36,9 @@ import android.view.ViewTreeObserver;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.systemui.Dumpable;
@@ -43,6 +46,8 @@ import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QS;
@@ -50,6 +55,8 @@ import com.android.systemui.plugins.qs.QSContainerController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.dagger.QSFragmentComponent;
import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
@@ -104,6 +111,10 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
private final QSFragmentComponent.Factory mQsComponentFactory;
private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger;
private final QSTileHost mHost;
private final FeatureFlags mFeatureFlags;
private final NewFooterActionsController mNewFooterActionsController;
private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory;
private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner;
private boolean mShowCollapsedOnKeyguard;
private boolean mLastKeyguardAndExpanded;
/**
@@ -119,8 +130,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
private QSPanelController mQSPanelController;
private QuickQSPanelController mQuickQSPanelController;
private QSCustomizerController mQSCustomizerController;
@Nullable
private FooterActionsController mQSFooterActionController;
@Nullable
private FooterActionsViewModel mQSFooterActionsViewModel;
@Nullable
private ScrollListener mScrollListener;
/**
* When true, QS will translate from outside the screen. It will be clipped with parallax
@@ -161,7 +175,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
KeyguardBypassController keyguardBypassController,
QSFragmentComponent.Factory qsComponentFactory,
QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger,
FalsingManager falsingManager, DumpManager dumpManager) {
FalsingManager falsingManager, DumpManager dumpManager, FeatureFlags featureFlags,
NewFooterActionsController newFooterActionsController,
FooterActionsViewModel.Factory footerActionsViewModelFactory) {
mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
mQsMediaHost = qsMediaHost;
mQqsMediaHost = qqsMediaHost;
@@ -173,6 +189,10 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
mBypassController = keyguardBypassController;
mStatusBarStateController = statusBarStateController;
mDumpManager = dumpManager;
mFeatureFlags = featureFlags;
mNewFooterActionsController = newFooterActionsController;
mFooterActionsViewModelFactory = footerActionsViewModelFactory;
mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
}
@Override
@@ -193,11 +213,22 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this);
mQSPanelController = qsFragmentComponent.getQSPanelController();
mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController();
mQSFooterActionController = qsFragmentComponent.getQSFooterActionController();
mQSPanelController.init();
mQuickQSPanelController.init();
mQSFooterActionController.init();
if (mFeatureFlags.isEnabled(Flags.NEW_FOOTER_ACTIONS)) {
mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */
this);
FooterActionsView footerActionsView = view.findViewById(R.id.qs_footer_actions);
FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
mListeningAndVisibilityLifecycleOwner);
mNewFooterActionsController.init();
} else {
mQSFooterActionController = qsFragmentComponent.getQSFooterActionController();
mQSFooterActionController.init();
}
mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view);
mQSPanelScrollView.addOnLayoutChangeListener(
@@ -283,6 +314,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
mDumpManager.unregisterDumpable(mContainer.getClass().getName());
}
mDumpManager.unregisterDumpable(getClass().getName());
mListeningAndVisibilityLifecycleOwner.destroy();
}
@Override
@@ -395,7 +427,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
mContainer.disable(state1, state2, animate);
mHeader.disable(state1, state2, animate);
mFooter.disable(state1, state2, animate);
mQSFooterActionController.disable(state2);
if (mQSFooterActionController != null) {
mQSFooterActionController.disable(state2);
}
updateQsState();
}
@@ -415,7 +449,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
boolean footerVisible = qsPanelVisible && (expanded || !keyguardShowing || mHeaderAnimating
|| mShowCollapsedOnKeyguard);
mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
mQSFooterActionController.setVisible(footerVisible);
if (mQSFooterActionController != null) {
mQSFooterActionController.setVisible(footerVisible);
} else {
mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible);
}
mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
|| (expanded && !mStackScrollerOverscrolling));
mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
@@ -482,7 +520,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
}
mFooter.setKeyguardShowing(keyguardShowing);
mQSFooterActionController.setKeyguardShowing(keyguardShowing);
if (mQSFooterActionController != null) {
mQSFooterActionController.setKeyguardShowing(keyguardShowing);
}
updateQsState();
}
@@ -498,7 +538,10 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
if (DEBUG) Log.d(TAG, "setListening " + listening);
mListening = listening;
mQSContainerImplController.setListening(listening && mQsVisible);
mQSFooterActionController.setListening(listening && mQsVisible);
if (mQSFooterActionController != null) {
mQSFooterActionController.setListening(listening && mQsVisible);
}
mListeningAndVisibilityLifecycleOwner.updateState();
updateQsPanelControllerListening();
}
@@ -511,6 +554,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
if (DEBUG) Log.d(TAG, "setQsVisible " + visible);
mQsVisible = visible;
setListening(mListening);
mListeningAndVisibilityLifecycleOwner.updateState();
}
@Override
@@ -602,7 +646,12 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
float footerActionsExpansion =
onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
mQSFooterActionController.setExpansion(footerActionsExpansion);
if (mQSFooterActionController != null) {
mQSFooterActionController.setExpansion(footerActionsExpansion);
} else {
mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
mInSplitShade);
}
mQSPanelController.setRevealExpansion(expansion);
mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
@@ -714,7 +763,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
boolean customizing = isCustomizing();
mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
mQSFooterActionController.setVisible(!customizing);
if (mQSFooterActionController != null) {
mQSFooterActionController.setVisible(!customizing);
} else {
mQSFooterActionsViewModel.onVisibilityChangeRequested(!customizing);
}
mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
// Let the panel know the position changed and it needs to update where notifications
// and whatnot are.
@@ -860,4 +913,56 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
}
return "GONE";
}
/**
* A {@link LifecycleOwner} whose state is driven by the current state of this fragment:
*
* - DESTROYED when the fragment is destroyed.
* - CREATED when mListening == mQsVisible == false.
* - STARTED when mListening == true && mQsVisible == false.
* - RESUMED when mListening == true && mQsVisible == true.
*/
private class ListeningAndVisibilityLifecycleOwner implements LifecycleOwner {
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
private boolean mDestroyed = false;
{
updateState();
}
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
/**
* Update the state of the associated lifecycle. This should be called whenever
* {@code mListening} or {@code mQsVisible} is changed.
*/
public void updateState() {
if (mDestroyed) {
mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
return;
}
if (!mListening) {
mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
return;
}
// mListening && !mQsVisible.
if (!mQsVisible) {
mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
return;
}
// mListening && mQsVisible.
mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
}
public void destroy() {
mDestroyed = true;
updateState();
}
}
}

View File

@@ -174,11 +174,12 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener {
@Inject
QSSecurityFooterUtils(
@Application Context context, UserTracker userTracker, @Main Handler mainHandler,
ActivityStarter activityStarter, SecurityController securityController,
@Background Looper bgLooper, DialogLaunchAnimator dialogLaunchAnimator) {
@Application Context context, DevicePolicyManager devicePolicyManager,
UserTracker userTracker, @Main Handler mainHandler, ActivityStarter activityStarter,
SecurityController securityController, @Background Looper bgLooper,
DialogLaunchAnimator dialogLaunchAnimator) {
mContext = context;
mDpm = mContext.getSystemService(DevicePolicyManager.class);
mDpm = devicePolicyManager;
mUserTracker = userTracker;
mMainHandler = mainHandler;
mActivityStarter = activityStarter;

View File

@@ -28,8 +28,6 @@ import com.android.systemui.battery.BatteryMeterView;
import com.android.systemui.dagger.qualifiers.RootView;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.privacy.OngoingPrivacyChip;
import com.android.systemui.qs.FgsManagerController;
import com.android.systemui.qs.FgsManagerControllerImpl;
import com.android.systemui.qs.FooterActionsView;
import com.android.systemui.qs.QSContainerImpl;
import com.android.systemui.qs.QSFooter;
@@ -196,8 +194,4 @@ public interface QSFragmentModule {
) {
return layoutInflater.inflate(R.layout.fgs_footer, footerActionsView, false);
}
/** */
@Binds
FgsManagerController bindFgsManagerController(FgsManagerControllerImpl impl);
}

View File

@@ -16,8 +16,6 @@
package com.android.systemui.qs.footer.dagger
import com.android.systemui.qs.FgsManagerController
import com.android.systemui.qs.FgsManagerControllerImpl
import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepositoryImpl
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
@@ -38,6 +36,4 @@ interface FooterActionsModule {
): ForegroundServicesRepository
@Binds fun footerActionsInteractor(impl: FooterActionsInteractorImpl): FooterActionsInteractor
@Binds fun fgsManagerControllerImpl(impl: FgsManagerControllerImpl): FgsManagerController
}

View File

@@ -0,0 +1,321 @@
/*
* Copyright (C) 2022 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.qs.footer.ui.binder
import android.content.Context
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.common.ui.ContentDescriptionViewBinder
import com.android.systemui.common.ui.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.people.ui.view.PeopleViewBinder.bind
import com.android.systemui.qs.FooterActionsView
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/** A ViewBinder for [FooterActionsViewBinder]. */
object FooterActionsViewBinder {
/**
* Create a [FooterActionsView] that can later be [bound][bind] to a [FooterActionsViewModel].
*/
@JvmStatic
fun create(context: Context): FooterActionsView {
return LayoutInflater.from(context).inflate(R.layout.footer_actions, /* root= */ null)
as FooterActionsView
}
/** Bind [view] to [viewModel]. */
@JvmStatic
fun bind(
view: FooterActionsView,
viewModel: FooterActionsViewModel,
qsVisibilityLifecycleOwner: LifecycleOwner,
) {
// Remove all children of the FooterActionsView that are used by the old implementation.
// TODO(b/242040009): Clean up the XML once the old implementation is removed.
view.removeAllViews()
// Add the views used by this new implementation.
val context = view.context
val inflater = LayoutInflater.from(context)
val securityHolder = TextButtonViewHolder.createAndAdd(inflater, view)
val foregroundServicesWithTextHolder = TextButtonViewHolder.createAndAdd(inflater, view)
val foregroundServicesWithNumberHolder = NumberButtonViewHolder.createAndAdd(inflater, view)
val userSwitcherHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = false)
val settingsHolder =
IconButtonViewHolder.createAndAdd(inflater, view, isLast = viewModel.power == null)
// Bind the static power and settings buttons.
bindButton(settingsHolder, viewModel.settings)
if (viewModel.power != null) {
val powerHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = true)
bindButton(powerHolder, viewModel.power)
}
// There are 2 lifecycle scopes we are using here:
// 1) The scope created by [repeatWhenAttached] when [view] is attached, and destroyed
// when the [view] is detached. We use this as the parent scope for all our [viewModel]
// state collection, given that we don't want to do any work when [view] is detached.
// 2) The scope owned by [lifecycleOwner], which should be RESUMED only when Quick
// Settings are visible. We use this to make sure we collect UI state only when the
// View is visible.
//
// Given that we start our collection when the Quick Settings become visible, which happens
// every time the user swipes down the shade, we remember our previous UI state already
// bound to the UI to avoid binding the same values over and over for nothing.
// TODO(b/242040009): Look into using only a single scope.
var previousSecurity: FooterActionsSecurityButtonViewModel? = null
var previousForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null
var previousUserSwitcher: FooterActionsButtonViewModel? = null
view.repeatWhenAttached {
val attachedScope = this.lifecycleScope
attachedScope.launch {
// Listen for dialog requests as soon as we are attached, even when not visible.
// TODO(b/242040009): Should this move somewhere else?
launch { viewModel.observeDeviceMonitoringDialogRequests(view.context) }
// Make sure we set the correct visibility and alpha even when QS are not currently
// shown.
launch {
viewModel.isVisible.collect { isVisible -> view.isInvisible = !isVisible }
}
launch { viewModel.alpha.collect { view.alpha = it } }
launch { viewModel.backgroundAlpha.collect { view.backgroundAlpha = it } }
}
// Listen for model changes only when QS are visible.
qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
// Security.
launch {
viewModel.security.collect { security ->
if (previousSecurity != security) {
bindSecurity(securityHolder, security)
previousSecurity = security
}
}
}
// Foreground services.
launch {
viewModel.foregroundServices.collect { foregroundServices ->
if (previousForegroundServices != foregroundServices) {
bindForegroundService(
foregroundServicesWithNumberHolder,
foregroundServicesWithTextHolder,
foregroundServices,
)
previousForegroundServices = foregroundServices
}
}
}
// User switcher.
launch {
viewModel.userSwitcher.collect { userSwitcher ->
if (previousUserSwitcher != userSwitcher) {
bindButton(userSwitcherHolder, userSwitcher)
previousUserSwitcher = userSwitcher
}
}
}
}
}
}
private fun bindSecurity(
securityHolder: TextButtonViewHolder,
security: FooterActionsSecurityButtonViewModel?,
) {
val securityView = securityHolder.view
securityView.isVisible = security != null
if (security == null) {
return
}
// Make sure that the chevron is visible and that the button is clickable if there is a
// listener.
val chevron = securityHolder.chevron
if (security.onClick != null) {
securityView.isClickable = true
securityView.setOnClickListener(security.onClick)
chevron.isVisible = true
} else {
securityView.isClickable = false
securityView.setOnClickListener(null)
chevron.isVisible = false
}
securityHolder.text.text = security.text
securityHolder.newDot.isVisible = false
IconViewBinder.bind(security.icon, securityHolder.icon)
}
private fun bindForegroundService(
foregroundServicesWithNumberHolder: NumberButtonViewHolder,
foregroundServicesWithTextHolder: TextButtonViewHolder,
foregroundServices: FooterActionsForegroundServicesButtonViewModel?,
) {
val foregroundServicesWithNumberView = foregroundServicesWithNumberHolder.view
val foregroundServicesWithTextView = foregroundServicesWithTextHolder.view
if (foregroundServices == null) {
foregroundServicesWithNumberView.isVisible = false
foregroundServicesWithTextView.isVisible = false
return
}
val foregroundServicesCount = foregroundServices.foregroundServicesCount
if (foregroundServices.displayText) {
// Button with text, icon and chevron.
foregroundServicesWithNumberView.isVisible = false
foregroundServicesWithTextView.isVisible = true
foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick)
foregroundServicesWithTextHolder.text.text = foregroundServices.text
foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges
} else {
// Small button with the number only.
foregroundServicesWithTextView.isVisible = false
foregroundServicesWithNumberView.visibility = View.VISIBLE
foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick)
foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges
}
}
private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) {
val buttonView = button.view
buttonView.isVisible = model != null
if (model == null) {
return
}
buttonView.setBackgroundResource(model.background)
buttonView.setOnClickListener(model.onClick)
val icon = model.icon
val iconView = button.icon
val contentDescription = model.contentDescription
IconViewBinder.bind(icon, iconView)
ContentDescriptionViewBinder.bind(contentDescription, iconView)
if (model.iconTint != null) {
iconView.setColorFilter(model.iconTint, PorterDuff.Mode.SRC_IN)
} else {
iconView.clearColorFilter()
}
}
}
private class TextButtonViewHolder(val view: View) {
val icon = view.requireViewById<ImageView>(R.id.icon)
val text = view.requireViewById<TextView>(R.id.text)
val newDot = view.requireViewById<ImageView>(R.id.new_dot)
val chevron = view.requireViewById<ImageView>(R.id.chevron_icon)
companion object {
fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): TextButtonViewHolder {
val view =
inflater.inflate(
R.layout.footer_actions_text_button,
/* root= */ root,
/* attachToRoot= */ false,
)
root.addView(view)
return TextButtonViewHolder(view)
}
}
}
private class NumberButtonViewHolder(val view: View) {
val number = view.requireViewById<TextView>(R.id.number)
val newDot = view.requireViewById<ImageView>(R.id.new_dot)
companion object {
fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): NumberButtonViewHolder {
val view =
inflater.inflate(
R.layout.footer_actions_number_button,
/* root= */ root,
/* attachToRoot= */ false,
)
root.addView(view)
return NumberButtonViewHolder(view)
}
}
}
private class IconButtonViewHolder(val view: View) {
val icon = view.requireViewById<ImageView>(R.id.icon)
companion object {
fun createAndAdd(
inflater: LayoutInflater,
root: ViewGroup,
isLast: Boolean,
): IconButtonViewHolder {
val view =
inflater.inflate(
R.layout.footer_actions_icon_button,
/* root= */ root,
/* attachToRoot= */ false,
)
// All buttons have a background with an inset of qs_footer_action_inset, so the last
// button must have a negative inset of -qs_footer_action_inset to compensate and be
// aligned with its parent.
val marginEnd =
if (isLast) {
-view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset)
} else {
0
}
val size =
view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_button_size)
root.addView(
view,
LinearLayout.LayoutParams(size, size).apply { this.marginEnd = marginEnd },
)
return IconButtonViewHolder(view)
}
}
}

View File

@@ -46,6 +46,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** A ViewModel for the footer actions. */
@@ -74,60 +75,65 @@ class FooterActionsViewModel(
/** The model for the security button. */
val security: Flow<FooterActionsSecurityButtonViewModel?> =
footerActionsInteractor.securityButtonConfig.map { config ->
val (icon, text, isClickable) = config ?: return@map null
FooterActionsSecurityButtonViewModel(
icon,
text,
if (isClickable) this::onSecurityButtonClicked else null,
)
}
footerActionsInteractor.securityButtonConfig
.map { config ->
val (icon, text, isClickable) = config ?: return@map null
FooterActionsSecurityButtonViewModel(
icon,
text,
if (isClickable) this::onSecurityButtonClicked else null,
)
}
.distinctUntilChanged()
/** The model for the foreground services button. */
val foregroundServices: Flow<FooterActionsForegroundServicesButtonViewModel?> =
combine(
footerActionsInteractor.foregroundServicesCount,
footerActionsInteractor.hasNewForegroundServices,
security,
) { foregroundServicesCount, hasNewChanges, securityModel ->
if (foregroundServicesCount <= 0) {
return@combine null
}
footerActionsInteractor.foregroundServicesCount,
footerActionsInteractor.hasNewForegroundServices,
security,
) { foregroundServicesCount, hasNewChanges, securityModel ->
if (foregroundServicesCount <= 0) {
return@combine null
}
val text =
icuMessageFormat(
context.resources,
R.string.fgs_manager_footer_label,
val text =
icuMessageFormat(
context.resources,
R.string.fgs_manager_footer_label,
foregroundServicesCount,
)
FooterActionsForegroundServicesButtonViewModel(
foregroundServicesCount,
text = text,
displayText = securityModel == null,
hasNewChanges = hasNewChanges,
this::onForegroundServiceButtonClicked,
)
FooterActionsForegroundServicesButtonViewModel(
foregroundServicesCount,
text = text,
displayText = securityModel == null,
hasNewChanges = hasNewChanges,
this::onForegroundServiceButtonClicked,
)
}
}
.distinctUntilChanged()
/** The model for the user switcher button. */
val userSwitcher: Flow<FooterActionsButtonViewModel?> =
footerActionsInteractor.userSwitcherStatus.map { userSwitcherStatus ->
when (userSwitcherStatus) {
UserSwitcherStatusModel.Disabled -> null
is UserSwitcherStatusModel.Enabled -> {
if (userSwitcherStatus.currentUserImage == null) {
Log.e(
TAG,
"Skipped the addition of user switcher button because " +
"currentUserImage is missing",
)
return@map null
}
footerActionsInteractor.userSwitcherStatus
.map { userSwitcherStatus ->
when (userSwitcherStatus) {
UserSwitcherStatusModel.Disabled -> null
is UserSwitcherStatusModel.Enabled -> {
if (userSwitcherStatus.currentUserImage == null) {
Log.e(
TAG,
"Skipped the addition of user switcher button because " +
"currentUserImage is missing",
)
return@map null
}
userSwitcherButton(userSwitcherStatus)
userSwitcherButton(userSwitcherStatus)
}
}
}
}
.distinctUntilChanged()
/** The model for the settings button. */
val settings: FooterActionsButtonViewModel =

View File

@@ -29,6 +29,7 @@ import com.android.systemui.R;
/**
* Container for image of the multi user switcher (tappable).
*/
// TODO(b/242040009): Remove this file.
public class MultiUserSwitch extends FrameLayout {
public MultiUserSwitch(Context context, AttributeSet attrs) {
super(context, attrs);

View File

@@ -40,6 +40,7 @@ import com.android.systemui.util.ViewController;
import javax.inject.Inject;
/** View Controller for {@link MultiUserSwitch}. */
// TODO(b/242040009): Remove this file.
public class MultiUserSwitchController extends ViewController<MultiUserSwitch> {
private final UserManager mUserManager;
private final UserSwitcherController mUserSwitcherController;

View File

@@ -45,12 +45,15 @@ import com.android.systemui.R;
import com.android.systemui.SysuiBaseFragmentTest;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.dagger.QSFragmentComponent;
import com.android.systemui.qs.external.TileServiceRequestController;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -390,6 +393,8 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
setUpMedia();
setUpOther();
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
featureFlags.set(Flags.NEW_FOOTER_ACTIONS, false);
return new QSFragment(
new RemoteInputQuickSettingsDisabler(
context, commandQueue, mock(ConfigurationController.class)),
@@ -402,7 +407,10 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
mQsComponentFactory,
mock(QSFragmentDisableFlagsLogger.class),
mFalsingManager,
mock(DumpManager.class));
mock(DumpManager.class),
featureFlags,
mock(NewFooterActionsController.class),
mock(FooterActionsViewModel.Factory.class));
}
private void setUpOther() {

View File

@@ -124,8 +124,9 @@ public class QSSecurityFooterTest extends SysuiTestCase {
mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext)
.replace("ImageView", TestableImageView.class)
.build().inflate(R.layout.quick_settings_security_footer, null, false);
mFooterUtils = new QSSecurityFooterUtils(getContext(), mUserTracker, mainHandler,
mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator);
mFooterUtils = new QSSecurityFooterUtils(getContext(),
getContext().getSystemService(DevicePolicyManager.class), mUserTracker,
mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator);
mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper,
mBroadcastDispatcher, mFooterUtils);
mFooterText = mRootView.findViewById(R.id.footer_text);