diff --git a/ktfmt_includes.txt b/ktfmt_includes.txt index 96da8c9c803b6..c7062e0931359 100644 --- a/ktfmt_includes.txt +++ b/ktfmt_includes.txt @@ -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 \ No newline at end of file +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/ \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/fgs_footer.xml b/packages/SystemUI/res-keyguard/layout/fgs_footer.xml index 6757acf7014ad..ee588f997ab84 100644 --- a/packages/SystemUI/res-keyguard/layout/fgs_footer.xml +++ b/packages/SystemUI/res-keyguard/layout/fgs_footer.xml @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> + + diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml new file mode 100644 index 0000000000000..fad41c822ec07 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml new file mode 100644 index 0000000000000..940b9e1fd4556 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml @@ -0,0 +1,39 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml new file mode 100644 index 0000000000000..27a1fa8f4a7b7 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/quick_settings_security_footer.xml b/packages/SystemUI/res/layout/quick_settings_security_footer.xml index 1b11816465ac0..194f3dd5dc269 100644 --- a/packages/SystemUI/res/layout/quick_settings_security_footer.xml +++ b/packages/SystemUI/res/layout/quick_settings_security_footer.xml @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> + view.contentDescription = contentDescription.description + is ContentDescription.Resource -> { + view.contentDescription = view.context.resources.getString(contentDescription.res) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index f32ea353bf506..42e88be2d34b3 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -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); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 7299aa597e604..7f28d4a2266c3 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -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 = diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt index c790cfe7b7b7c..9d64781ef2e9a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt index 309ac2a66e6b9..d602b0b279774 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt new file mode 100644 index 0000000000000..7c67d9f42b55a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/NewFooterActionsController.kt @@ -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() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java index 372550656d571..7511278e0919b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 139fb8b0bc14e..05b3eae1d2f2e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -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(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java index 43b53b5cf1913..f6322743eaa11 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java index 6f6e4b428e949..aa505fb0b6bd4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java @@ -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); } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt index 000c23d2e592c..38fe34eb8f9f5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/dagger/FooterActionsModule.kt @@ -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 } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt new file mode 100644 index 0000000000000..484ae0a9c93bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt @@ -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(R.id.icon) + val text = view.requireViewById(R.id.text) + val newDot = view.requireViewById(R.id.new_dot) + val chevron = view.requireViewById(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(R.id.number) + val newDot = view.requireViewById(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(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) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index 8afe6f216cbb4..b556a3e0d66b6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -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 = - 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 = 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 = - 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 = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java index 5168533cd2b71..365fbace16080 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitch.java @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java index 799e5feb1586d..4d6168989691a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java @@ -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 { private final UserManager mUserManager; private final UserSwitcherController mUserSwitcherController; diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index 10f6ce8c0ec9d..f08ad2453c5fc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -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() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java index a156a609c0508..233c267c3be07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java @@ -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);