Merge changes from topics "footer-actions-fakes", "footer-actions-mad", "footer-actions-screenshots" into tm-qpr-dev
* changes: Enable the new footer actions in team food. Add screenshot test for the footer actions (2/2) Implement LaunchableView for footer actions Views. Wire the new FooterActions implementation behind a flag Implementation of the FooterActions following the MAD (1/3) Add BroadcastDispatcher.broadcastFlow Extract QSSecurityFooterUtils out of QSSecurityFooter Make the logic of QSSecurityFooter reusable Add some fakes to SystemUI-test-utils Allow preventing unrestricted Apps from being Stopped
This commit is contained in:
committed by
Android (Google) Code Review
commit
6fbd48df33
@@ -548,6 +548,12 @@ public final class SystemUiDeviceConfigFlags {
|
||||
*/
|
||||
public static final String TASK_MANAGER_SHOW_FOOTER_DOT = "task_manager_show_footer_dot";
|
||||
|
||||
/**
|
||||
* (boolean) Whether the task manager should show a stop button if the app is allowlisted
|
||||
* by the user.
|
||||
*/
|
||||
public static final String TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS =
|
||||
"show_stop_button_for_user_allowlisted_apps";
|
||||
|
||||
/**
|
||||
* (boolean) Whether the clipboard overlay is enabled.
|
||||
|
||||
@@ -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/
|
||||
@@ -230,6 +230,7 @@ android_library {
|
||||
libs: [
|
||||
"android.test.runner",
|
||||
"android.test.base",
|
||||
"android.test.mock",
|
||||
],
|
||||
kotlincflags: ["-Xjvm-default=enable"],
|
||||
aaptflags: [
|
||||
|
||||
@@ -311,8 +311,7 @@ class ActivityLaunchAnimator(
|
||||
@JvmStatic
|
||||
fun fromView(view: View, cujType: Int? = null): Controller? {
|
||||
if (view.parent !is ViewGroup) {
|
||||
// TODO(b/192194319): Throw instead of just logging.
|
||||
Log.wtf(
|
||||
Log.e(
|
||||
TAG,
|
||||
"Skipping animation as view $view is not attached to a ViewGroup",
|
||||
Exception()
|
||||
|
||||
@@ -113,6 +113,19 @@ constructor(
|
||||
}
|
||||
val animateFrom = animatedParent?.dialogContentWithBackground ?: view
|
||||
|
||||
if (animatedParent == null && animateFrom !is LaunchableView) {
|
||||
// Make sure the View we launch from implements LaunchableView to avoid visibility
|
||||
// issues. Given that we don't own dialog decorViews so we can't enforce it for launches
|
||||
// from a dialog.
|
||||
// TODO(b/243636422): Throw instead of logging to enforce this.
|
||||
Log.w(
|
||||
TAG,
|
||||
"A dialog was launched from a View that does not implement LaunchableView. This " +
|
||||
"can lead to subtle bugs where the visibility of the View we are " +
|
||||
"launching from is not what we expected."
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure we don't run the launch animation from the same view twice at the same time.
|
||||
if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
|
||||
Log.e(TAG, "Not running dialog launch animation as there is already one running")
|
||||
@@ -156,9 +169,14 @@ constructor(
|
||||
openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
|
||||
?: throw IllegalStateException(
|
||||
"The animateFrom dialog was not animated using " +
|
||||
"DialogLaunchAnimator.showFrom(View|Dialog)")
|
||||
"DialogLaunchAnimator.showFrom(View|Dialog)"
|
||||
)
|
||||
showFromView(
|
||||
dialog, view, animateBackgroundBoundsChange = animateBackgroundBoundsChange, cuj = cuj)
|
||||
dialog,
|
||||
view,
|
||||
animateBackgroundBoundsChange = animateBackgroundBoundsChange,
|
||||
cuj = cuj
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,7 +215,7 @@ constructor(
|
||||
// bouncer.
|
||||
if (
|
||||
!dialog.isShowing ||
|
||||
(!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
|
||||
(!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
|
||||
) {
|
||||
return null
|
||||
}
|
||||
@@ -556,11 +574,12 @@ private class AnimatedDialog(
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
|
||||
viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
|
||||
val type = if (wasFittingNavigationBars) {
|
||||
WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars()
|
||||
} else {
|
||||
WindowInsets.Type.displayCutout()
|
||||
}
|
||||
val type =
|
||||
if (wasFittingNavigationBars) {
|
||||
WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars()
|
||||
} else {
|
||||
WindowInsets.Type.displayCutout()
|
||||
}
|
||||
|
||||
val insets = windowInsets.getInsets(type)
|
||||
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.animation
|
||||
|
||||
import android.view.View
|
||||
|
||||
/** A piece of UI that can be expanded into a Dialog or an Activity. */
|
||||
interface Expandable {
|
||||
/**
|
||||
* Create an [ActivityLaunchAnimator.Controller] that can be used to expand this [Expandable]
|
||||
* into an Activity, or return `null` if this [Expandable] should not be animated (e.g. if it is
|
||||
* currently not attached or visible).
|
||||
*
|
||||
* @param cujType the CUJ type from the [com.android.internal.jank.InteractionJankMonitor]
|
||||
* associated to the launch that will use this controller.
|
||||
*/
|
||||
fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller?
|
||||
|
||||
// TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here.
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create an [Expandable] that will animate [view] when expanded.
|
||||
*
|
||||
* Note: The background of [view] should be a (rounded) rectangle so that it can be properly
|
||||
* animated.
|
||||
*/
|
||||
fun fromView(view: View): Expandable {
|
||||
return object : Expandable {
|
||||
override fun activityLaunchController(
|
||||
cujType: Int?,
|
||||
): ActivityLaunchAnimator.Controller? {
|
||||
return ActivityLaunchAnimator.Controller.fromView(view, cujType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,79 @@
|
||||
|
||||
package com.android.systemui.animation
|
||||
|
||||
import android.view.View
|
||||
|
||||
/** A view that can expand/launch into an app or a dialog. */
|
||||
interface LaunchableView {
|
||||
/**
|
||||
* Set whether this view should block/prevent all visibility changes. This ensures that this
|
||||
* view remains invisible during the launch animation given that it is ghosted and already drawn
|
||||
* Set whether this view should block/postpone all visibility changes. This ensures that this
|
||||
* view:
|
||||
* - remains invisible during the launch animation given that it is ghosted and already drawn
|
||||
* somewhere else.
|
||||
* - remains invisible as long as a dialog expanded from it is shown.
|
||||
* - restores its expected visibility once the dialog expanded from it is dismissed.
|
||||
*
|
||||
* Note that when this is set to true, both the [normal][android.view.View.setVisibility] and
|
||||
* [transition][android.view.View.setTransitionVisibility] visibility changes must be blocked.
|
||||
*
|
||||
* @param block whether we should block/postpone all calls to `setVisibility` and
|
||||
* `setTransitionVisibility`.
|
||||
*/
|
||||
fun setShouldBlockVisibilityChanges(block: Boolean)
|
||||
}
|
||||
|
||||
/** A delegate that can be used by views to make the implementation of [LaunchableView] easier. */
|
||||
class LaunchableViewDelegate(
|
||||
private val view: View,
|
||||
|
||||
/**
|
||||
* The lambda that should set the actual visibility of [view], usually by calling
|
||||
* super.setVisibility(visibility).
|
||||
*/
|
||||
private val superSetVisibility: (Int) -> Unit,
|
||||
|
||||
/**
|
||||
* The lambda that should set the actual transition visibility of [view], usually by calling
|
||||
* super.setTransitionVisibility(visibility).
|
||||
*/
|
||||
private val superSetTransitionVisibility: (Int) -> Unit,
|
||||
) {
|
||||
private var blockVisibilityChanges = false
|
||||
private var lastVisibility = view.visibility
|
||||
|
||||
/** Call this when [LaunchableView.setShouldBlockVisibilityChanges] is called. */
|
||||
fun setShouldBlockVisibilityChanges(block: Boolean) {
|
||||
if (block == blockVisibilityChanges) {
|
||||
return
|
||||
}
|
||||
|
||||
blockVisibilityChanges = block
|
||||
if (block) {
|
||||
lastVisibility = view.visibility
|
||||
} else {
|
||||
superSetVisibility(lastVisibility)
|
||||
}
|
||||
}
|
||||
|
||||
/** Call this when [View.setVisibility] is called. */
|
||||
fun setVisibility(visibility: Int) {
|
||||
if (blockVisibilityChanges) {
|
||||
lastVisibility = visibility
|
||||
return
|
||||
}
|
||||
|
||||
superSetVisibility(visibility)
|
||||
}
|
||||
|
||||
/** Call this when [View.setTransitionVisibility] is called. */
|
||||
fun setTransitionVisibility(visibility: Int) {
|
||||
if (blockVisibilityChanges) {
|
||||
// View.setTransitionVisibility just sets the visibility flag, so we don't have to save
|
||||
// the transition visibility separately from the normal visibility.
|
||||
lastVisibility = visibility
|
||||
return
|
||||
}
|
||||
|
||||
superSetTransitionVisibility(visibility)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ android_library {
|
||||
"testables",
|
||||
"truth-prebuilt",
|
||||
"androidx.test.uiautomator",
|
||||
"kotlinx_coroutines_test",
|
||||
],
|
||||
|
||||
libs: [
|
||||
"android.test.mock",
|
||||
],
|
||||
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.UserHandle
|
||||
import android.view.View
|
||||
import com.android.internal.util.UserIcons
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.animation.Expandable
|
||||
import com.android.systemui.classifier.FalsingManagerFake
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.globalactions.GlobalActionsDialogLite
|
||||
import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
|
||||
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
|
||||
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
|
||||
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
/** A list of fake [FooterActionsViewModel] to be used in screenshot tests and the gallery. */
|
||||
fun fakeFooterActionsViewModels(
|
||||
@Application context: Context,
|
||||
): List<FooterActionsViewModel> {
|
||||
return listOf(
|
||||
fakeFooterActionsViewModel(context),
|
||||
fakeFooterActionsViewModel(context, showPowerButton = false, isGuestUser = true),
|
||||
fakeFooterActionsViewModel(context, showUserSwitcher = false),
|
||||
fakeFooterActionsViewModel(context, showUserSwitcher = false, foregroundServices = 4),
|
||||
fakeFooterActionsViewModel(
|
||||
context,
|
||||
foregroundServices = 4,
|
||||
hasNewForegroundServices = true,
|
||||
userId = 1,
|
||||
),
|
||||
fakeFooterActionsViewModel(
|
||||
context,
|
||||
securityText = "Security",
|
||||
foregroundServices = 4,
|
||||
showUserSwitcher = false,
|
||||
),
|
||||
fakeFooterActionsViewModel(
|
||||
context,
|
||||
securityText = "Security (not clickable)",
|
||||
securityClickable = false,
|
||||
foregroundServices = 4,
|
||||
hasNewForegroundServices = true,
|
||||
userId = 2,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun fakeFooterActionsViewModel(
|
||||
@Application context: Context,
|
||||
securityText: String? = null,
|
||||
securityClickable: Boolean = true,
|
||||
foregroundServices: Int = 0,
|
||||
hasNewForegroundServices: Boolean = false,
|
||||
showUserSwitcher: Boolean = true,
|
||||
showPowerButton: Boolean = true,
|
||||
userId: Int = UserHandle.USER_OWNER,
|
||||
isGuestUser: Boolean = false,
|
||||
): FooterActionsViewModel {
|
||||
val interactor =
|
||||
FakeFooterActionsInteractor(
|
||||
securityButtonConfig =
|
||||
flowOf(
|
||||
securityText?.let { text ->
|
||||
SecurityButtonConfig(
|
||||
icon = Icon.Resource(R.drawable.ic_info_outline),
|
||||
text = text,
|
||||
isClickable = securityClickable,
|
||||
)
|
||||
}
|
||||
),
|
||||
foregroundServicesCount = flowOf(foregroundServices),
|
||||
hasNewForegroundServices = flowOf(hasNewForegroundServices),
|
||||
userSwitcherStatus =
|
||||
flowOf(
|
||||
if (showUserSwitcher) {
|
||||
UserSwitcherStatusModel.Enabled(
|
||||
currentUserName = "foo",
|
||||
currentUserImage =
|
||||
UserIcons.getDefaultUserIcon(
|
||||
context.resources,
|
||||
userId,
|
||||
/* light= */ false,
|
||||
),
|
||||
isGuestUser = isGuestUser,
|
||||
)
|
||||
} else {
|
||||
UserSwitcherStatusModel.Disabled
|
||||
}
|
||||
),
|
||||
deviceMonitoringDialogRequests = flowOf(),
|
||||
)
|
||||
|
||||
return FooterActionsViewModel(
|
||||
context,
|
||||
interactor,
|
||||
FalsingManagerFake(),
|
||||
globalActionsDialogLite = mock(),
|
||||
showPowerButton = showPowerButton,
|
||||
)
|
||||
}
|
||||
|
||||
private class FakeFooterActionsInteractor(
|
||||
override val securityButtonConfig: Flow<SecurityButtonConfig?> = flowOf(null),
|
||||
override val foregroundServicesCount: Flow<Int> = flowOf(0),
|
||||
override val hasNewForegroundServices: Flow<Boolean> = flowOf(false),
|
||||
override val userSwitcherStatus: Flow<UserSwitcherStatusModel> =
|
||||
flowOf(UserSwitcherStatusModel.Disabled),
|
||||
override val deviceMonitoringDialogRequests: Flow<Unit> = flowOf(),
|
||||
private val onShowDeviceMonitoringDialogFromView: (View) -> Unit = {},
|
||||
private val onShowDeviceMonitoringDialog: (Context) -> Unit = {},
|
||||
private val onShowForegroundServicesDialog: (View) -> Unit = {},
|
||||
private val onShowPowerMenuDialog: (GlobalActionsDialogLite, View) -> Unit = { _, _ -> },
|
||||
private val onShowSettings: (Expandable) -> Unit = {},
|
||||
private val onShowUserSwitcher: (View) -> Unit = {},
|
||||
) : FooterActionsInteractor {
|
||||
override fun showDeviceMonitoringDialog(view: View) {
|
||||
onShowDeviceMonitoringDialogFromView(view)
|
||||
}
|
||||
|
||||
override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
|
||||
onShowDeviceMonitoringDialog(quickSettingsContext)
|
||||
}
|
||||
|
||||
override fun showForegroundServicesDialog(view: View) {
|
||||
onShowForegroundServicesDialog(view)
|
||||
}
|
||||
|
||||
override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) {
|
||||
onShowPowerMenuDialog(globalActionsDialogLite, view)
|
||||
}
|
||||
|
||||
override fun showSettings(expandable: Expandable) {
|
||||
onShowSettings(expandable)
|
||||
}
|
||||
|
||||
override fun showUserSwitcher(view: View) {
|
||||
onShowUserSwitcher(view)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -16,16 +16,17 @@
|
||||
-->
|
||||
|
||||
<!-- 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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/footer_actions_height"
|
||||
android:elevation="@dimen/qs_panel_elevation"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingTop="@dimen/qs_footer_actions_top_padding"
|
||||
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"
|
||||
>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
-->
|
||||
<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: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" />
|
||||
</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
|
||||
@@ -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.
|
||||
-->
|
||||
<com.android.systemui.common.ui.view.LaunchableLinearLayout
|
||||
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" />
|
||||
</com.android.systemui.common.ui.view.LaunchableLinearLayout>
|
||||
@@ -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"
|
||||
|
||||
@@ -380,6 +380,7 @@
|
||||
|
||||
<!-- (48dp - 40dp) / 2 -->
|
||||
<dimen name="qs_footer_action_inset">4dp</dimen>
|
||||
<dimen name="qs_footer_actions_top_padding">8dp</dimen>
|
||||
<dimen name="qs_footer_actions_bottom_padding">4dp</dimen>
|
||||
<dimen name="qs_footer_action_inset_negative">-4dp</dimen>
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ import android.util.SparseArray
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.systemui.Dumpable
|
||||
import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger
|
||||
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
|
||||
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dump.DumpManager
|
||||
@@ -38,6 +40,8 @@ import com.android.systemui.settings.UserTracker
|
||||
import java.io.PrintWriter
|
||||
import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
data class ReceiverData(
|
||||
val receiver: BroadcastReceiver,
|
||||
@@ -153,6 +157,55 @@ open class BroadcastDispatcher @Inject constructor(
|
||||
.sendToTarget()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that, when collected, emits a new value whenever a broadcast matching
|
||||
* [filter] is received. The value will be computed from the intent and the registered receiver
|
||||
* using [map].
|
||||
*
|
||||
* @see registerReceiver
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun <T> broadcastFlow(
|
||||
filter: IntentFilter,
|
||||
user: UserHandle? = null,
|
||||
@Context.RegisterReceiverFlags flags: Int = Context.RECEIVER_EXPORTED,
|
||||
permission: String? = null,
|
||||
map: (Intent, BroadcastReceiver) -> T,
|
||||
): Flow<T> = conflatedCallbackFlow {
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
trySendWithFailureLogging(map(intent, this), TAG)
|
||||
}
|
||||
}
|
||||
|
||||
registerReceiver(
|
||||
receiver,
|
||||
filter,
|
||||
bgExecutor,
|
||||
user,
|
||||
flags,
|
||||
permission,
|
||||
)
|
||||
|
||||
awaitClose {
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Flow] that, when collected, emits `Unit` whenever a broadcast matching [filter] is
|
||||
* received.
|
||||
*
|
||||
* @see registerReceiver
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun broadcastFlow(
|
||||
filter: IntentFilter,
|
||||
user: UserHandle? = null,
|
||||
@Context.RegisterReceiverFlags flags: Int = Context.RECEIVER_EXPORTED,
|
||||
permission: String? = null,
|
||||
): Flow<Unit> = broadcastFlow(filter, user, flags, permission) { _, _ -> Unit }
|
||||
|
||||
private fun checkFilter(filter: IntentFilter) {
|
||||
val sb = StringBuilder()
|
||||
if (filter.countActions() == 0) sb.append("Filter must contain at least one action. ")
|
||||
|
||||
@@ -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.common.shared.model
|
||||
|
||||
import android.annotation.StringRes
|
||||
|
||||
/**
|
||||
* Models a content description, that can either be already [loaded][ContentDescription.Loaded] or
|
||||
* be a [reference][ContentDescription.Resource] to a resource.
|
||||
*/
|
||||
sealed class ContentDescription {
|
||||
data class Loaded(
|
||||
val description: String?,
|
||||
) : ContentDescription()
|
||||
|
||||
data class Resource(
|
||||
@StringRes val res: Int,
|
||||
) : ContentDescription()
|
||||
}
|
||||
@@ -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.shared.model
|
||||
|
||||
import android.annotation.DrawableRes
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
/**
|
||||
* Models an icon, that can either be already [loaded][Icon.Loaded] or be a [reference]
|
||||
* [Icon.Resource] to a resource.
|
||||
*/
|
||||
sealed class Icon {
|
||||
data class Loaded(
|
||||
val drawable: Drawable,
|
||||
) : Icon()
|
||||
|
||||
data class Resource(
|
||||
@DrawableRes val res: Int,
|
||||
) : Icon()
|
||||
}
|
||||
@@ -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.binder
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.binder
|
||||
|
||||
import android.widget.ImageView
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
|
||||
object IconViewBinder {
|
||||
fun bind(
|
||||
icon: Icon,
|
||||
view: ImageView,
|
||||
) {
|
||||
when (icon) {
|
||||
is Icon.Loaded -> view.setImageDrawable(icon.drawable)
|
||||
is Icon.Resource -> view.setImageResource(icon.res)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
import com.android.systemui.animation.LaunchableView
|
||||
import com.android.systemui.animation.LaunchableViewDelegate
|
||||
|
||||
/** A [LinearLayout] that also implements [LaunchableView]. */
|
||||
class LaunchableLinearLayout : LinearLayout, LaunchableView {
|
||||
private val delegate =
|
||||
LaunchableViewDelegate(
|
||||
this,
|
||||
superSetVisibility = { super.setVisibility(it) },
|
||||
superSetTransitionVisibility = { super.setTransitionVisibility(it) },
|
||||
)
|
||||
|
||||
constructor(context: Context?) : super(context)
|
||||
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(
|
||||
context: Context?,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int,
|
||||
) : super(context, attrs, defStyleAttr)
|
||||
|
||||
constructor(
|
||||
context: Context?,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int,
|
||||
defStyleRes: Int,
|
||||
) : super(context, attrs, defStyleAttr, defStyleRes)
|
||||
|
||||
override fun setShouldBlockVisibilityChanges(block: Boolean) {
|
||||
delegate.setShouldBlockVisibilityChanges(block)
|
||||
}
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
delegate.setVisibility(visibility)
|
||||
}
|
||||
|
||||
override fun setTransitionVisibility(visibility: Int) {
|
||||
delegate.setTransitionVisibility(visibility)
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,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;
|
||||
@@ -122,6 +126,7 @@ import dagger.Provides;
|
||||
DemoModeModule.class,
|
||||
FalsingModule.class,
|
||||
FlagsModule.class,
|
||||
FooterActionsModule.class,
|
||||
LogModule.class,
|
||||
MediaProjectionModule.class,
|
||||
PeopleHubModule.class,
|
||||
@@ -132,6 +137,7 @@ import dagger.Provides;
|
||||
ScreenshotModule.class,
|
||||
SensorModule.class,
|
||||
MultiUserUtilsModule.class,
|
||||
SecurityRepositoryModule.class,
|
||||
SettingsUtilModule.class,
|
||||
SmartRepliesInflationModule.class,
|
||||
SmartspaceModule.class,
|
||||
@@ -258,4 +264,7 @@ public abstract class SystemUIModule {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract FgsManagerController bindFgsManagerController(FgsManagerControllerImpl impl);
|
||||
}
|
||||
|
||||
@@ -151,6 +151,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, true);
|
||||
|
||||
/***************************************/
|
||||
// 600- status bar
|
||||
public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =
|
||||
|
||||
@@ -40,11 +40,13 @@ import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.GuardedBy
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_FOOTER_DOT
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS
|
||||
import com.android.internal.jank.InteractionJankMonitor
|
||||
import com.android.systemui.Dumpable
|
||||
import com.android.systemui.R
|
||||
@@ -66,9 +68,73 @@ import java.util.Objects
|
||||
import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/** A controller for the dealing with services running in the foreground. */
|
||||
interface FgsManagerController {
|
||||
/** Whether the TaskManager (and therefore this controller) is actually available. */
|
||||
val isAvailable: StateFlow<Boolean>
|
||||
|
||||
/** The number of packages with a service running in the foreground. */
|
||||
val numRunningPackages: Int
|
||||
|
||||
/**
|
||||
* Whether there were new changes to the foreground services since the last [shown][showDialog]
|
||||
* dialog was dismissed.
|
||||
*/
|
||||
val newChangesSinceDialogWasDismissed: Boolean
|
||||
|
||||
/**
|
||||
* Whether we should show a dot to indicate when [newChangesSinceDialogWasDismissed] is true.
|
||||
*/
|
||||
val showFooterDot: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Initialize this controller. This should be called once, before this controller is used for
|
||||
* the first time.
|
||||
*/
|
||||
fun init()
|
||||
|
||||
/**
|
||||
* Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if
|
||||
* it's not `null`.
|
||||
*/
|
||||
fun showDialog(viewLaunchedFrom: View?)
|
||||
|
||||
/** Add a [OnNumberOfPackagesChangedListener]. */
|
||||
fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
|
||||
|
||||
/** Remove a [OnNumberOfPackagesChangedListener]. */
|
||||
fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
|
||||
|
||||
/** Add a [OnDialogDismissedListener]. */
|
||||
fun addOnDialogDismissedListener(listener: OnDialogDismissedListener)
|
||||
|
||||
/** Remove a [OnDialogDismissedListener]. */
|
||||
fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener)
|
||||
|
||||
/** Whether we should update the footer visibility. */
|
||||
// TODO(b/242040009): Remove this.
|
||||
fun shouldUpdateFooterVisibility(): Boolean
|
||||
|
||||
@VisibleForTesting
|
||||
fun visibleButtonsCount(): Int
|
||||
|
||||
interface OnNumberOfPackagesChangedListener {
|
||||
/** Called when [numRunningPackages] changed. */
|
||||
fun onNumberOfPackagesChanged(numPackages: Int)
|
||||
}
|
||||
|
||||
interface OnDialogDismissedListener {
|
||||
/** Called when a dialog shown using [showDialog] was dismissed. */
|
||||
fun onDialogDismissed()
|
||||
}
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
class FgsManagerController @Inject constructor(
|
||||
class FgsManagerControllerImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
@Main private val mainExecutor: Executor,
|
||||
@Background private val backgroundExecutor: Executor,
|
||||
@@ -80,22 +146,32 @@ class FgsManagerController @Inject constructor(
|
||||
private val dialogLaunchAnimator: DialogLaunchAnimator,
|
||||
private val broadcastDispatcher: BroadcastDispatcher,
|
||||
private val dumpManager: DumpManager
|
||||
) : IForegroundServiceObserver.Stub(), Dumpable {
|
||||
) : IForegroundServiceObserver.Stub(), Dumpable, FgsManagerController {
|
||||
|
||||
companion object {
|
||||
private const val INTERACTION_JANK_TAG = "active_background_apps"
|
||||
private val LOG_TAG = FgsManagerController::class.java.simpleName
|
||||
private const val DEFAULT_TASK_MANAGER_ENABLED = true
|
||||
private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false
|
||||
private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true
|
||||
}
|
||||
|
||||
var changesSinceDialog = false
|
||||
override var newChangesSinceDialogWasDismissed = false
|
||||
private set
|
||||
|
||||
var isAvailable = false
|
||||
private set
|
||||
var showFooterDot = false
|
||||
private set
|
||||
val _isAvailable = MutableStateFlow(false)
|
||||
override val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow()
|
||||
|
||||
val _showFooterDot = MutableStateFlow(false)
|
||||
override val showFooterDot: StateFlow<Boolean> = _showFooterDot.asStateFlow()
|
||||
|
||||
private var showStopBtnForUserAllowlistedApps = false
|
||||
|
||||
override val numRunningPackages: Int
|
||||
get() {
|
||||
synchronized(lock) {
|
||||
return getNumVisiblePackagesLocked()
|
||||
}
|
||||
}
|
||||
|
||||
private val lock = Any()
|
||||
|
||||
@@ -133,15 +209,7 @@ class FgsManagerController @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
interface OnNumberOfPackagesChangedListener {
|
||||
fun onNumberOfPackagesChanged(numPackages: Int)
|
||||
}
|
||||
|
||||
interface OnDialogDismissedListener {
|
||||
fun onDialogDismissed()
|
||||
}
|
||||
|
||||
fun init() {
|
||||
override fun init() {
|
||||
synchronized(lock) {
|
||||
if (initialized) {
|
||||
return
|
||||
@@ -160,19 +228,26 @@ class FgsManagerController @Inject constructor(
|
||||
NAMESPACE_SYSTEMUI,
|
||||
backgroundExecutor
|
||||
) {
|
||||
isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable)
|
||||
showFooterDot =
|
||||
it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, showFooterDot)
|
||||
_isAvailable.value = it.getBoolean(TASK_MANAGER_ENABLED, _isAvailable.value)
|
||||
_showFooterDot.value =
|
||||
it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, _showFooterDot.value)
|
||||
showStopBtnForUserAllowlistedApps = it.getBoolean(
|
||||
TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
|
||||
showStopBtnForUserAllowlistedApps)
|
||||
}
|
||||
|
||||
isAvailable = deviceConfigProxy.getBoolean(
|
||||
_isAvailable.value = deviceConfigProxy.getBoolean(
|
||||
NAMESPACE_SYSTEMUI,
|
||||
TASK_MANAGER_ENABLED, DEFAULT_TASK_MANAGER_ENABLED
|
||||
)
|
||||
showFooterDot = deviceConfigProxy.getBoolean(
|
||||
_showFooterDot.value = deviceConfigProxy.getBoolean(
|
||||
NAMESPACE_SYSTEMUI,
|
||||
TASK_MANAGER_SHOW_FOOTER_DOT, DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT
|
||||
)
|
||||
showStopBtnForUserAllowlistedApps = deviceConfigProxy.getBoolean(
|
||||
NAMESPACE_SYSTEMUI,
|
||||
TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
|
||||
DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS)
|
||||
|
||||
dumpManager.registerDumpable(this)
|
||||
|
||||
@@ -220,42 +295,45 @@ class FgsManagerController @Inject constructor(
|
||||
}
|
||||
|
||||
@GuardedBy("lock")
|
||||
val onNumberOfPackagesChangedListeners: MutableSet<OnNumberOfPackagesChangedListener> =
|
||||
mutableSetOf()
|
||||
private val onNumberOfPackagesChangedListeners =
|
||||
mutableSetOf<FgsManagerController.OnNumberOfPackagesChangedListener>()
|
||||
|
||||
@GuardedBy("lock")
|
||||
val onDialogDismissedListeners: MutableSet<OnDialogDismissedListener> = mutableSetOf()
|
||||
private val onDialogDismissedListeners =
|
||||
mutableSetOf<FgsManagerController.OnDialogDismissedListener>()
|
||||
|
||||
fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
|
||||
override fun addOnNumberOfPackagesChangedListener(
|
||||
listener: FgsManagerController.OnNumberOfPackagesChangedListener
|
||||
) {
|
||||
synchronized(lock) {
|
||||
onNumberOfPackagesChangedListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
|
||||
override fun removeOnNumberOfPackagesChangedListener(
|
||||
listener: FgsManagerController.OnNumberOfPackagesChangedListener
|
||||
) {
|
||||
synchronized(lock) {
|
||||
onNumberOfPackagesChangedListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun addOnDialogDismissedListener(listener: OnDialogDismissedListener) {
|
||||
override fun addOnDialogDismissedListener(
|
||||
listener: FgsManagerController.OnDialogDismissedListener
|
||||
) {
|
||||
synchronized(lock) {
|
||||
onDialogDismissedListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener) {
|
||||
override fun removeOnDialogDismissedListener(
|
||||
listener: FgsManagerController.OnDialogDismissedListener
|
||||
) {
|
||||
synchronized(lock) {
|
||||
onDialogDismissedListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNumRunningPackages(): Int {
|
||||
synchronized(lock) {
|
||||
return getNumVisiblePackagesLocked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNumVisiblePackagesLocked(): Int {
|
||||
return runningServiceTokens.keys.count {
|
||||
it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId)
|
||||
@@ -266,7 +344,7 @@ class FgsManagerController @Inject constructor(
|
||||
val num = getNumVisiblePackagesLocked()
|
||||
if (num != lastNumberOfVisiblePackages) {
|
||||
lastNumberOfVisiblePackages = num
|
||||
changesSinceDialog = true
|
||||
newChangesSinceDialogWasDismissed = true
|
||||
onNumberOfPackagesChangedListeners.forEach {
|
||||
backgroundExecutor.execute {
|
||||
it.onNumberOfPackagesChanged(num)
|
||||
@@ -275,9 +353,21 @@ class FgsManagerController @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldUpdateFooterVisibility() = dialog == null
|
||||
override fun visibleButtonsCount(): Int {
|
||||
synchronized(lock) {
|
||||
return getNumVisibleButtonsLocked()
|
||||
}
|
||||
}
|
||||
|
||||
fun showDialog(viewLaunchedFrom: View?) {
|
||||
private fun getNumVisibleButtonsLocked(): Int {
|
||||
return runningServiceTokens.keys.count {
|
||||
it.uiControl != UIControl.HIDE_BUTTON && currentProfileIds.contains(it.userId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shouldUpdateFooterVisibility() = dialog == null
|
||||
|
||||
override fun showDialog(viewLaunchedFrom: View?) {
|
||||
synchronized(lock) {
|
||||
if (dialog == null) {
|
||||
|
||||
@@ -302,7 +392,7 @@ class FgsManagerController @Inject constructor(
|
||||
this.dialog = dialog
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
changesSinceDialog = false
|
||||
newChangesSinceDialogWasDismissed = false
|
||||
synchronized(lock) {
|
||||
this.dialog = null
|
||||
updateAppItemsLocked()
|
||||
@@ -505,6 +595,13 @@ class FgsManagerController @Inject constructor(
|
||||
PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI,
|
||||
PowerExemptionManager.REASON_ROLE_DIALER,
|
||||
PowerExemptionManager.REASON_SYSTEM_MODULE -> UIControl.HIDE_BUTTON
|
||||
|
||||
PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE ->
|
||||
if (showStopBtnForUserAllowlistedApps) {
|
||||
UIControl.NORMAL
|
||||
} else {
|
||||
UIControl.HIDE_BUTTON
|
||||
}
|
||||
else -> UIControl.NORMAL
|
||||
}
|
||||
uiControlInitialized = true
|
||||
@@ -623,7 +720,7 @@ class FgsManagerController @Inject constructor(
|
||||
val pw = IndentingPrintWriter(printwriter)
|
||||
synchronized(lock) {
|
||||
pw.println("current user profiles = $currentProfileIds")
|
||||
pw.println("changesSinceDialog=$changesSinceDialog")
|
||||
pw.println("newChangesSinceDialogWasShown=$newChangesSinceDialogWasDismissed")
|
||||
pw.println("Running service tokens: [")
|
||||
pw.indentIfPossible {
|
||||
runningServiceTokens.forEach { (userPackage, startTimeAndTokens) ->
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -149,9 +150,11 @@ public class QSFgsManagerFooter implements View.OnClickListener,
|
||||
mNumberView.setContentDescription(text);
|
||||
if (mFgsManagerController.shouldUpdateFooterVisibility()) {
|
||||
mRootView.setVisibility(mNumPackages > 0
|
||||
&& mFgsManagerController.isAvailable() ? View.VISIBLE : View.GONE);
|
||||
int dotVis = mFgsManagerController.getShowFooterDot()
|
||||
&& mFgsManagerController.getChangesSinceDialog() ? View.VISIBLE : View.GONE;
|
||||
&& mFgsManagerController.isAvailable().getValue() ? View.VISIBLE
|
||||
: View.GONE);
|
||||
int dotVis = mFgsManagerController.getShowFooterDot().getValue()
|
||||
&& mFgsManagerController.getNewChangesSinceDialogWasDismissed()
|
||||
? View.VISIBLE : View.GONE;
|
||||
mDotView.setVisibility(dotVis);
|
||||
mCollapsedDotView.setVisibility(dotVis);
|
||||
if (mVisibilityChangedListener != null) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,126 +15,66 @@
|
||||
*/
|
||||
package com.android.systemui.qs;
|
||||
|
||||
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_CA_CERT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NETWORK;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TITLE;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_CA_CERT_SUBTITLE;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_NETWORK_SUBTITLE;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_VPN_SUBTITLE;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_NAMED_MANAGEMENT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_VIEW_POLICIES;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_CA_CERT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NETWORK;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MONITORING;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MULTIPLE_VPNS;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MONITORING;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_WORK_PROFILE_MONITORING;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_PERSONAL_PROFILE_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_MONITORING;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NETWORK;
|
||||
|
||||
import static com.android.systemui.qs.dagger.QSFragmentModule.QS_SECURITY_FOOTER_VIEW;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.admin.DeviceAdminInfo;
|
||||
import android.app.admin.DevicePolicyEventLogger;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.Window;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.internal.jank.InteractionJankMonitor;
|
||||
import com.android.internal.util.FrameworkStatsLog;
|
||||
import com.android.systemui.FontSizeUtils;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.DialogCuj;
|
||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||
import com.android.systemui.common.shared.model.Icon;
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
import com.android.systemui.qs.dagger.QSScope;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
import com.android.systemui.statusbar.phone.SystemUIDialog;
|
||||
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig;
|
||||
import com.android.systemui.security.data.model.SecurityModel;
|
||||
import com.android.systemui.statusbar.policy.SecurityController;
|
||||
import com.android.systemui.util.ViewController;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
/** ViewController for the footer actions. */
|
||||
// TODO(b/242040009): Remove this class.
|
||||
@QSScope
|
||||
class QSSecurityFooter extends ViewController<View>
|
||||
implements OnClickListener, DialogInterface.OnClickListener,
|
||||
VisibilityChangedDispatcher {
|
||||
public class QSSecurityFooter extends ViewController<View>
|
||||
implements OnClickListener, VisibilityChangedDispatcher {
|
||||
protected static final String TAG = "QSSecurityFooter";
|
||||
protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
private static final boolean DEBUG_FORCE_VISIBLE = false;
|
||||
|
||||
private static final String INTERACTION_JANK_TAG = "managed_device_info";
|
||||
|
||||
private final TextView mFooterText;
|
||||
private final ImageView mPrimaryFooterIcon;
|
||||
private Context mContext;
|
||||
private final DevicePolicyManager mDpm;
|
||||
private final Callback mCallback = new Callback();
|
||||
private final SecurityController mSecurityController;
|
||||
private final ActivityStarter mActivityStarter;
|
||||
private final Handler mMainHandler;
|
||||
private final UserTracker mUserTracker;
|
||||
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
||||
private final BroadcastDispatcher mBroadcastDispatcher;
|
||||
private final QSSecurityFooterUtils mQSSecurityFooterUtils;
|
||||
|
||||
private final AtomicBoolean mShouldUseSettingsButton = new AtomicBoolean(false);
|
||||
|
||||
private AlertDialog mDialog;
|
||||
protected H mHandler;
|
||||
|
||||
private boolean mIsVisible;
|
||||
private boolean mIsClickable;
|
||||
@Nullable
|
||||
private CharSequence mFooterTextContent = null;
|
||||
private int mFooterIconId;
|
||||
@Nullable
|
||||
private Drawable mPrimaryFooterIconDrawable;
|
||||
private Icon mFooterIcon;
|
||||
|
||||
@Nullable
|
||||
private VisibilityChangedDispatcher.OnVisibilityChangedListener mVisibilityChangedListener;
|
||||
@@ -149,82 +89,21 @@ class QSSecurityFooter extends ViewController<View>
|
||||
}
|
||||
};
|
||||
|
||||
private Supplier<String> mManagementTitleSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(R.string.monitoring_title_device_owned);
|
||||
|
||||
private Supplier<String> mManagementMessageSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_management);
|
||||
|
||||
private Supplier<String> mManagementMonitoringStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_management_monitoring);
|
||||
|
||||
private Supplier<String> mManagementMultipleVpnStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_management_vpns);
|
||||
|
||||
private Supplier<String> mWorkProfileMonitoringStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_managed_profile_monitoring);
|
||||
|
||||
private Supplier<String> mWorkProfileNetworkStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_managed_profile_network_activity);
|
||||
|
||||
private Supplier<String> mMonitoringSubtitleCaCertStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_subtitle_ca_certificate);
|
||||
|
||||
private Supplier<String> mMonitoringSubtitleNetworkStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_subtitle_network_logging);
|
||||
|
||||
private Supplier<String> mMonitoringSubtitleVpnStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(R.string.monitoring_subtitle_vpn);
|
||||
|
||||
private Supplier<String> mViewPoliciesButtonStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(R.string.monitoring_button_view_policies);
|
||||
|
||||
private Supplier<String> mManagementDialogStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_management);
|
||||
|
||||
private Supplier<String> mManagementDialogCaCertStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_management_ca_certificate);
|
||||
|
||||
private Supplier<String> mWorkProfileDialogCaCertStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_managed_profile_ca_certificate);
|
||||
|
||||
private Supplier<String> mManagementDialogNetworkStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_management_network_logging);
|
||||
|
||||
private Supplier<String> mWorkProfileDialogNetworkStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_managed_profile_network_logging);
|
||||
|
||||
@Inject
|
||||
QSSecurityFooter(@Named(QS_SECURITY_FOOTER_VIEW) View rootView,
|
||||
UserTracker userTracker, @Main Handler mainHandler,
|
||||
ActivityStarter activityStarter, SecurityController securityController,
|
||||
DialogLaunchAnimator dialogLaunchAnimator, @Background Looper bgLooper,
|
||||
BroadcastDispatcher broadcastDispatcher) {
|
||||
@Main Handler mainHandler, SecurityController securityController,
|
||||
@Background Looper bgLooper, BroadcastDispatcher broadcastDispatcher,
|
||||
QSSecurityFooterUtils qSSecurityFooterUtils) {
|
||||
super(rootView);
|
||||
mFooterText = mView.findViewById(R.id.footer_text);
|
||||
mPrimaryFooterIcon = mView.findViewById(R.id.primary_footer_icon);
|
||||
mFooterIconId = R.drawable.ic_info_outline;
|
||||
mFooterIcon = new Icon.Resource(R.drawable.ic_info_outline);
|
||||
mContext = rootView.getContext();
|
||||
mDpm = rootView.getContext().getSystemService(DevicePolicyManager.class);
|
||||
mMainHandler = mainHandler;
|
||||
mActivityStarter = activityStarter;
|
||||
mSecurityController = securityController;
|
||||
mMainHandler = mainHandler;
|
||||
mHandler = new H(bgLooper);
|
||||
mUserTracker = userTracker;
|
||||
mDialogLaunchAnimator = dialogLaunchAnimator;
|
||||
mBroadcastDispatcher = broadcastDispatcher;
|
||||
mQSSecurityFooterUtils = qSSecurityFooterUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -287,8 +166,9 @@ class QSSecurityFooter extends ViewController<View>
|
||||
.write();
|
||||
}
|
||||
|
||||
// TODO(b/242040009): Remove this.
|
||||
public void showDeviceMonitoringDialog() {
|
||||
createDialog();
|
||||
mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView);
|
||||
}
|
||||
|
||||
public void refreshState() {
|
||||
@@ -296,590 +176,30 @@ class QSSecurityFooter extends ViewController<View>
|
||||
}
|
||||
|
||||
private void handleRefreshState() {
|
||||
final boolean isDeviceManaged = mSecurityController.isDeviceManaged();
|
||||
final UserInfo currentUser = mUserTracker.getUserInfo();
|
||||
final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null
|
||||
&& currentUser.isDemo();
|
||||
final boolean hasWorkProfile = mSecurityController.hasWorkProfile();
|
||||
final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser();
|
||||
final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile();
|
||||
final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled();
|
||||
final String vpnName = mSecurityController.getPrimaryVpnName();
|
||||
final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName();
|
||||
final CharSequence organizationName = mSecurityController.getDeviceOwnerOrganizationName();
|
||||
final CharSequence workProfileOrganizationName =
|
||||
mSecurityController.getWorkProfileOrganizationName();
|
||||
final boolean isProfileOwnerOfOrganizationOwnedDevice =
|
||||
mSecurityController.isProfileOwnerOfOrganizationOwnedDevice();
|
||||
final boolean isParentalControlsEnabled = mSecurityController.isParentalControlsEnabled();
|
||||
final boolean isWorkProfileOn = mSecurityController.isWorkProfileOn();
|
||||
final boolean hasDisclosableWorkProfilePolicy = hasCACertsInWorkProfile
|
||||
|| vpnNameWorkProfile != null || (hasWorkProfile && isNetworkLoggingEnabled);
|
||||
// Update visibility of footer
|
||||
mIsVisible = (isDeviceManaged && !isDemoDevice)
|
||||
|| hasCACerts
|
||||
|| vpnName != null
|
||||
|| isProfileOwnerOfOrganizationOwnedDevice
|
||||
|| isParentalControlsEnabled
|
||||
|| (hasDisclosableWorkProfilePolicy && isWorkProfileOn);
|
||||
// Update the view to be untappable if the device is an organization-owned device with a
|
||||
// managed profile and there is either:
|
||||
// a) no policy set which requires a privacy disclosure.
|
||||
// b) a specific work policy set but the work profile is turned off.
|
||||
if (mIsVisible && isProfileOwnerOfOrganizationOwnedDevice
|
||||
&& (!hasDisclosableWorkProfilePolicy || !isWorkProfileOn)) {
|
||||
mView.setClickable(false);
|
||||
mView.findViewById(R.id.footer_icon).setVisibility(View.GONE);
|
||||
SecurityModel securityModel = SecurityModel.create(mSecurityController);
|
||||
SecurityButtonConfig buttonConfig = mQSSecurityFooterUtils.getButtonConfig(securityModel);
|
||||
|
||||
if (buttonConfig == null) {
|
||||
mIsVisible = false;
|
||||
} else {
|
||||
mView.setClickable(true);
|
||||
mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE);
|
||||
}
|
||||
// Update the string
|
||||
mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile,
|
||||
hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName,
|
||||
vpnNameWorkProfile, organizationName, workProfileOrganizationName,
|
||||
isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled,
|
||||
isWorkProfileOn);
|
||||
// Update the icon
|
||||
int footerIconId = R.drawable.ic_info_outline;
|
||||
if (vpnName != null || vpnNameWorkProfile != null) {
|
||||
if (mSecurityController.isVpnBranded()) {
|
||||
footerIconId = R.drawable.stat_sys_branded_vpn;
|
||||
} else {
|
||||
footerIconId = R.drawable.stat_sys_vpn_ic;
|
||||
}
|
||||
}
|
||||
if (mFooterIconId != footerIconId) {
|
||||
mFooterIconId = footerIconId;
|
||||
mIsVisible = true;
|
||||
mIsClickable = buttonConfig.isClickable();
|
||||
mFooterTextContent = buttonConfig.getText();
|
||||
mFooterIcon = buttonConfig.getIcon();
|
||||
}
|
||||
|
||||
// Update the primary icon
|
||||
if (isParentalControlsEnabled) {
|
||||
if (mPrimaryFooterIconDrawable == null) {
|
||||
DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo();
|
||||
mPrimaryFooterIconDrawable = mSecurityController.getIcon(info);
|
||||
}
|
||||
} else {
|
||||
mPrimaryFooterIconDrawable = null;
|
||||
}
|
||||
// Update the UI.
|
||||
mMainHandler.post(mUpdatePrimaryIcon);
|
||||
|
||||
mMainHandler.post(mUpdateDisplayState);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getFooterText(boolean isDeviceManaged, boolean hasWorkProfile,
|
||||
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
|
||||
String vpnName, String vpnNameWorkProfile, CharSequence organizationName,
|
||||
CharSequence workProfileOrganizationName,
|
||||
boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isParentalControlsEnabled,
|
||||
boolean isWorkProfileOn) {
|
||||
if (isParentalControlsEnabled) {
|
||||
return mContext.getString(R.string.quick_settings_disclosure_parental_controls);
|
||||
}
|
||||
if (isDeviceManaged || DEBUG_FORCE_VISIBLE) {
|
||||
return getManagedDeviceFooterText(hasCACerts, hasCACertsInWorkProfile,
|
||||
isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, organizationName);
|
||||
}
|
||||
return getManagedAndPersonalProfileFooterText(hasWorkProfile, hasCACerts,
|
||||
hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile,
|
||||
workProfileOrganizationName, isProfileOwnerOfOrganizationOwnedDevice,
|
||||
isWorkProfileOn);
|
||||
}
|
||||
|
||||
private String getManagedDeviceFooterText(
|
||||
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
|
||||
String vpnName, String vpnNameWorkProfile, CharSequence organizationName) {
|
||||
if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) {
|
||||
return getManagedDeviceMonitoringText(organizationName);
|
||||
}
|
||||
if (vpnName != null || vpnNameWorkProfile != null) {
|
||||
return getManagedDeviceVpnText(vpnName, vpnNameWorkProfile, organizationName);
|
||||
}
|
||||
return getMangedDeviceGeneralText(organizationName);
|
||||
}
|
||||
|
||||
private String getManagedDeviceMonitoringText(CharSequence organizationName) {
|
||||
if (organizationName == null) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_MANAGEMENT_MONITORING, mManagementMonitoringStringSupplier);
|
||||
}
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_MANAGEMENT_MONITORING,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_management_monitoring,
|
||||
organizationName),
|
||||
organizationName);
|
||||
}
|
||||
|
||||
private String getManagedDeviceVpnText(
|
||||
String vpnName, String vpnNameWorkProfile, CharSequence organizationName) {
|
||||
if (vpnName != null && vpnNameWorkProfile != null) {
|
||||
if (organizationName == null) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_MANAGEMENT_MULTIPLE_VPNS, mManagementMultipleVpnStringSupplier);
|
||||
}
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_management_vpns,
|
||||
organizationName),
|
||||
organizationName);
|
||||
}
|
||||
String name = vpnName != null ? vpnName : vpnNameWorkProfile;
|
||||
if (organizationName == null) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_MANAGEMENT_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_management_named_vpn,
|
||||
name),
|
||||
name);
|
||||
}
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_MANAGEMENT_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_management_named_vpn,
|
||||
organizationName,
|
||||
name),
|
||||
organizationName,
|
||||
name);
|
||||
}
|
||||
|
||||
private String getMangedDeviceGeneralText(CharSequence organizationName) {
|
||||
if (organizationName == null) {
|
||||
return mDpm.getResources().getString(QS_MSG_MANAGEMENT, mManagementMessageSupplier);
|
||||
}
|
||||
if (isFinancedDevice()) {
|
||||
return mContext.getString(
|
||||
R.string.quick_settings_financed_disclosure_named_management,
|
||||
organizationName);
|
||||
} else {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_MANAGEMENT,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_management,
|
||||
organizationName),
|
||||
organizationName);
|
||||
}
|
||||
}
|
||||
|
||||
private String getManagedAndPersonalProfileFooterText(boolean hasWorkProfile,
|
||||
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
|
||||
String vpnName, String vpnNameWorkProfile, CharSequence workProfileOrganizationName,
|
||||
boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isWorkProfileOn) {
|
||||
if (hasCACerts || (hasCACertsInWorkProfile && isWorkProfileOn)) {
|
||||
return getMonitoringText(
|
||||
hasCACerts, hasCACertsInWorkProfile, workProfileOrganizationName,
|
||||
isWorkProfileOn);
|
||||
}
|
||||
if (vpnName != null || (vpnNameWorkProfile != null && isWorkProfileOn)) {
|
||||
return getVpnText(hasWorkProfile, vpnName, vpnNameWorkProfile, isWorkProfileOn);
|
||||
}
|
||||
if (hasWorkProfile && isNetworkLoggingEnabled && isWorkProfileOn) {
|
||||
return getManagedProfileNetworkActivityText();
|
||||
}
|
||||
if (isProfileOwnerOfOrganizationOwnedDevice) {
|
||||
return getMangedDeviceGeneralText(workProfileOrganizationName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getMonitoringText(boolean hasCACerts, boolean hasCACertsInWorkProfile,
|
||||
CharSequence workProfileOrganizationName, boolean isWorkProfileOn) {
|
||||
if (hasCACertsInWorkProfile && isWorkProfileOn) {
|
||||
if (workProfileOrganizationName == null) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_WORK_PROFILE_MONITORING, mWorkProfileMonitoringStringSupplier);
|
||||
}
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_WORK_PROFILE_MONITORING,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_managed_profile_monitoring,
|
||||
workProfileOrganizationName),
|
||||
workProfileOrganizationName);
|
||||
}
|
||||
if (hasCACerts) {
|
||||
return mContext.getString(R.string.quick_settings_disclosure_monitoring);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getVpnText(boolean hasWorkProfile, String vpnName, String vpnNameWorkProfile,
|
||||
boolean isWorkProfileOn) {
|
||||
if (vpnName != null && vpnNameWorkProfile != null) {
|
||||
return mContext.getString(R.string.quick_settings_disclosure_vpns);
|
||||
}
|
||||
if (vpnNameWorkProfile != null && isWorkProfileOn) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_WORK_PROFILE_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_managed_profile_named_vpn,
|
||||
vpnNameWorkProfile),
|
||||
vpnNameWorkProfile);
|
||||
}
|
||||
if (vpnName != null) {
|
||||
if (hasWorkProfile) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_PERSONAL_PROFILE_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_personal_profile_named_vpn,
|
||||
vpnName),
|
||||
vpnName);
|
||||
}
|
||||
return mContext.getString(R.string.quick_settings_disclosure_named_vpn,
|
||||
vpnName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getManagedProfileNetworkActivityText() {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_WORK_PROFILE_NETWORK, mWorkProfileNetworkStringSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (which == DialogInterface.BUTTON_NEGATIVE) {
|
||||
final Intent intent = new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS);
|
||||
dialog.dismiss();
|
||||
// This dismisses the shade on opening the activity
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void createDialog() {
|
||||
mShouldUseSettingsButton.set(false);
|
||||
mHandler.post(() -> {
|
||||
String settingsButtonText = getSettingsButton();
|
||||
final View view = createDialogView();
|
||||
mMainHandler.post(() -> {
|
||||
mDialog = new SystemUIDialog(mContext, 0); // Use mContext theme
|
||||
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this);
|
||||
mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, mShouldUseSettingsButton.get()
|
||||
? settingsButtonText : getNegativeButton(), this);
|
||||
|
||||
mDialog.setView(view);
|
||||
if (mView.isAggregatedVisible()) {
|
||||
mDialogLaunchAnimator.showFromView(mDialog, mView, new DialogCuj(
|
||||
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG));
|
||||
} else {
|
||||
mDialog.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Dialog getDialog() {
|
||||
return mDialog;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
View createDialogView() {
|
||||
if (mSecurityController.isParentalControlsEnabled()) {
|
||||
return createParentalControlsDialogView();
|
||||
}
|
||||
return createOrganizationDialogView();
|
||||
}
|
||||
|
||||
private View createOrganizationDialogView() {
|
||||
final boolean isDeviceManaged = mSecurityController.isDeviceManaged();
|
||||
final boolean hasWorkProfile = mSecurityController.hasWorkProfile();
|
||||
final CharSequence deviceOwnerOrganization =
|
||||
mSecurityController.getDeviceOwnerOrganizationName();
|
||||
final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser();
|
||||
final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile();
|
||||
final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled();
|
||||
final String vpnName = mSecurityController.getPrimaryVpnName();
|
||||
final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName();
|
||||
|
||||
View dialogView = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.quick_settings_footer_dialog, null, false);
|
||||
|
||||
// device management section
|
||||
TextView deviceManagementSubtitle =
|
||||
dialogView.findViewById(R.id.device_management_subtitle);
|
||||
deviceManagementSubtitle.setText(getManagementTitle(deviceOwnerOrganization));
|
||||
|
||||
CharSequence managementMessage = getManagementMessage(isDeviceManaged,
|
||||
deviceOwnerOrganization);
|
||||
if (managementMessage == null) {
|
||||
dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.GONE);
|
||||
} else {
|
||||
dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.VISIBLE);
|
||||
TextView deviceManagementWarning =
|
||||
(TextView) dialogView.findViewById(R.id.device_management_warning);
|
||||
deviceManagementWarning.setText(managementMessage);
|
||||
mShouldUseSettingsButton.set(true);
|
||||
}
|
||||
|
||||
// ca certificate section
|
||||
CharSequence caCertsMessage = getCaCertsMessage(isDeviceManaged, hasCACerts,
|
||||
hasCACertsInWorkProfile);
|
||||
if (caCertsMessage == null) {
|
||||
dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.GONE);
|
||||
} else {
|
||||
dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.VISIBLE);
|
||||
TextView caCertsWarning = (TextView) dialogView.findViewById(R.id.ca_certs_warning);
|
||||
caCertsWarning.setText(caCertsMessage);
|
||||
// Make "Open trusted credentials"-link clickable
|
||||
caCertsWarning.setMovementMethod(new LinkMovementMethod());
|
||||
|
||||
TextView caCertsSubtitle = (TextView) dialogView.findViewById(R.id.ca_certs_subtitle);
|
||||
String caCertsSubtitleMessage = mDpm.getResources().getString(
|
||||
QS_DIALOG_MONITORING_CA_CERT_SUBTITLE, mMonitoringSubtitleCaCertStringSupplier);
|
||||
caCertsSubtitle.setText(caCertsSubtitleMessage);
|
||||
|
||||
}
|
||||
|
||||
// network logging section
|
||||
CharSequence networkLoggingMessage = getNetworkLoggingMessage(isDeviceManaged,
|
||||
isNetworkLoggingEnabled);
|
||||
if (networkLoggingMessage == null) {
|
||||
dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.GONE);
|
||||
} else {
|
||||
dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.VISIBLE);
|
||||
TextView networkLoggingWarning =
|
||||
(TextView) dialogView.findViewById(R.id.network_logging_warning);
|
||||
networkLoggingWarning.setText(networkLoggingMessage);
|
||||
|
||||
TextView networkLoggingSubtitle = (TextView) dialogView.findViewById(
|
||||
R.id.network_logging_subtitle);
|
||||
String networkLoggingSubtitleMessage = mDpm.getResources().getString(
|
||||
QS_DIALOG_MONITORING_NETWORK_SUBTITLE,
|
||||
mMonitoringSubtitleNetworkStringSupplier);
|
||||
networkLoggingSubtitle.setText(networkLoggingSubtitleMessage);
|
||||
}
|
||||
|
||||
// vpn section
|
||||
CharSequence vpnMessage = getVpnMessage(isDeviceManaged, hasWorkProfile, vpnName,
|
||||
vpnNameWorkProfile);
|
||||
if (vpnMessage == null) {
|
||||
dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.GONE);
|
||||
} else {
|
||||
dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.VISIBLE);
|
||||
TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning);
|
||||
vpnWarning.setText(vpnMessage);
|
||||
// Make "Open VPN Settings"-link clickable
|
||||
vpnWarning.setMovementMethod(new LinkMovementMethod());
|
||||
|
||||
TextView vpnSubtitle = (TextView) dialogView.findViewById(R.id.vpn_subtitle);
|
||||
String vpnSubtitleMessage = mDpm.getResources().getString(
|
||||
QS_DIALOG_MONITORING_VPN_SUBTITLE, mMonitoringSubtitleVpnStringSupplier);
|
||||
vpnSubtitle.setText(vpnSubtitleMessage);
|
||||
}
|
||||
|
||||
// Note: if a new section is added, should update configSubtitleVisibility to include
|
||||
// the handling of the subtitle
|
||||
configSubtitleVisibility(managementMessage != null,
|
||||
caCertsMessage != null,
|
||||
networkLoggingMessage != null,
|
||||
vpnMessage != null,
|
||||
dialogView);
|
||||
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
private View createParentalControlsDialogView() {
|
||||
View dialogView = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false);
|
||||
|
||||
DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo();
|
||||
Drawable icon = mSecurityController.getIcon(info);
|
||||
if (icon != null) {
|
||||
ImageView imageView = (ImageView) dialogView.findViewById(R.id.parental_controls_icon);
|
||||
imageView.setImageDrawable(icon);
|
||||
}
|
||||
|
||||
TextView parentalControlsTitle =
|
||||
(TextView) dialogView.findViewById(R.id.parental_controls_title);
|
||||
parentalControlsTitle.setText(mSecurityController.getLabel(info));
|
||||
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts,
|
||||
boolean showNetworkLogging, boolean showVpn, View dialogView) {
|
||||
// Device Management title should always been shown
|
||||
// When there is a Device Management message, all subtitles should be shown
|
||||
if (showDeviceManagement) {
|
||||
return;
|
||||
}
|
||||
// Hide the subtitle if there is only 1 message shown
|
||||
int mSectionCountExcludingDeviceMgt = 0;
|
||||
if (showCaCerts) { mSectionCountExcludingDeviceMgt++; }
|
||||
if (showNetworkLogging) { mSectionCountExcludingDeviceMgt++; }
|
||||
if (showVpn) { mSectionCountExcludingDeviceMgt++; }
|
||||
|
||||
// No work needed if there is no sections or more than 1 section
|
||||
if (mSectionCountExcludingDeviceMgt != 1) {
|
||||
return;
|
||||
}
|
||||
if (showCaCerts) {
|
||||
dialogView.findViewById(R.id.ca_certs_subtitle).setVisibility(View.GONE);
|
||||
}
|
||||
if (showNetworkLogging) {
|
||||
dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE);
|
||||
}
|
||||
if (showVpn) {
|
||||
dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// This should not be called on the main thread to avoid making an IPC.
|
||||
@VisibleForTesting
|
||||
String getSettingsButton() {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_VIEW_POLICIES, mViewPoliciesButtonStringSupplier);
|
||||
}
|
||||
|
||||
private String getPositiveButton() {
|
||||
return mContext.getString(R.string.ok);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getNegativeButton() {
|
||||
if (mSecurityController.isParentalControlsEnabled()) {
|
||||
return mContext.getString(R.string.monitoring_button_view_controls);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getManagementMessage(boolean isDeviceManaged,
|
||||
CharSequence organizationName) {
|
||||
if (!isDeviceManaged) {
|
||||
return null;
|
||||
}
|
||||
if (organizationName != null) {
|
||||
if (isFinancedDevice()) {
|
||||
return mContext.getString(R.string.monitoring_financed_description_named_management,
|
||||
organizationName, organizationName);
|
||||
} else {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_NAMED_MANAGEMENT,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_named_management,
|
||||
organizationName),
|
||||
organizationName);
|
||||
}
|
||||
}
|
||||
return mDpm.getResources().getString(QS_DIALOG_MANAGEMENT, mManagementDialogStringSupplier);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts,
|
||||
boolean hasCACertsInWorkProfile) {
|
||||
if (!(hasCACerts || hasCACertsInWorkProfile)) return null;
|
||||
if (isDeviceManaged) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_CA_CERT, mManagementDialogCaCertStringSupplier);
|
||||
}
|
||||
if (hasCACertsInWorkProfile) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_WORK_PROFILE_CA_CERT, mWorkProfileDialogCaCertStringSupplier);
|
||||
}
|
||||
return mContext.getString(R.string.monitoring_description_ca_certificate);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getNetworkLoggingMessage(boolean isDeviceManaged,
|
||||
boolean isNetworkLoggingEnabled) {
|
||||
if (!isNetworkLoggingEnabled) return null;
|
||||
if (isDeviceManaged) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_NETWORK, mManagementDialogNetworkStringSupplier);
|
||||
} else {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_WORK_PROFILE_NETWORK, mWorkProfileDialogNetworkStringSupplier);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile,
|
||||
String vpnName, String vpnNameWorkProfile) {
|
||||
if (vpnName == null && vpnNameWorkProfile == null) return null;
|
||||
final SpannableStringBuilder message = new SpannableStringBuilder();
|
||||
if (isDeviceManaged) {
|
||||
if (vpnName != null && vpnNameWorkProfile != null) {
|
||||
String namedVpns = mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_two_named_vpns,
|
||||
vpnName, vpnNameWorkProfile),
|
||||
vpnName, vpnNameWorkProfile);
|
||||
message.append(namedVpns);
|
||||
} else {
|
||||
String name = vpnName != null ? vpnName : vpnNameWorkProfile;
|
||||
String namedVp = mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_NAMED_VPN,
|
||||
() -> mContext.getString(R.string.monitoring_description_named_vpn, name),
|
||||
name);
|
||||
message.append(namedVp);
|
||||
}
|
||||
} else {
|
||||
if (vpnName != null && vpnNameWorkProfile != null) {
|
||||
String namedVpns = mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_two_named_vpns,
|
||||
vpnName, vpnNameWorkProfile),
|
||||
vpnName, vpnNameWorkProfile);
|
||||
message.append(namedVpns);
|
||||
} else if (vpnNameWorkProfile != null) {
|
||||
String namedVpn = mDpm.getResources().getString(
|
||||
QS_DIALOG_WORK_PROFILE_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_managed_profile_named_vpn,
|
||||
vpnNameWorkProfile),
|
||||
vpnNameWorkProfile);
|
||||
message.append(namedVpn);
|
||||
} else if (hasWorkProfile) {
|
||||
String namedVpn = mDpm.getResources().getString(
|
||||
QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_personal_profile_named_vpn,
|
||||
vpnName),
|
||||
vpnName);
|
||||
message.append(namedVpn);
|
||||
} else {
|
||||
message.append(mContext.getString(R.string.monitoring_description_named_vpn,
|
||||
vpnName));
|
||||
}
|
||||
}
|
||||
message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator));
|
||||
message.append(mContext.getString(R.string.monitoring_description_vpn_settings),
|
||||
new VpnSpan(), 0);
|
||||
return message;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
CharSequence getManagementTitle(CharSequence deviceOwnerOrganization) {
|
||||
if (deviceOwnerOrganization != null && isFinancedDevice()) {
|
||||
return mContext.getString(R.string.monitoring_title_financed_device,
|
||||
deviceOwnerOrganization);
|
||||
} else {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_TITLE,
|
||||
mManagementTitleSupplier);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFinancedDevice() {
|
||||
return mSecurityController.isDeviceManaged()
|
||||
&& mSecurityController.getDeviceOwnerType(
|
||||
mSecurityController.getDeviceOwnerComponentOnAnyUser())
|
||||
== DEVICE_OWNER_TYPE_FINANCED;
|
||||
}
|
||||
|
||||
private final Runnable mUpdatePrimaryIcon = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mPrimaryFooterIconDrawable != null) {
|
||||
mPrimaryFooterIcon.setImageDrawable(mPrimaryFooterIconDrawable);
|
||||
} else {
|
||||
mPrimaryFooterIcon.setImageResource(mFooterIconId);
|
||||
if (mFooterIcon instanceof Icon.Loaded) {
|
||||
mPrimaryFooterIcon.setImageDrawable(((Icon.Loaded) mFooterIcon).getDrawable());
|
||||
} else if (mFooterIcon instanceof Icon.Resource) {
|
||||
mPrimaryFooterIcon.setImageResource(((Icon.Resource) mFooterIcon).getRes());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -890,10 +210,18 @@ class QSSecurityFooter extends ViewController<View>
|
||||
if (mFooterTextContent != null) {
|
||||
mFooterText.setText(mFooterTextContent);
|
||||
}
|
||||
mView.setVisibility(mIsVisible || DEBUG_FORCE_VISIBLE ? View.VISIBLE : View.GONE);
|
||||
mView.setVisibility(mIsVisible ? View.VISIBLE : View.GONE);
|
||||
if (mVisibilityChangedListener != null) {
|
||||
mVisibilityChangedListener.onVisibilityChanged(mView.getVisibility());
|
||||
}
|
||||
|
||||
if (mIsVisible && mIsClickable) {
|
||||
mView.setClickable(true);
|
||||
mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mView.setClickable(false);
|
||||
mView.findViewById(R.id.footer_icon).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -929,25 +257,4 @@ class QSSecurityFooter extends ViewController<View>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class VpnSpan extends ClickableSpan {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS);
|
||||
mDialog.dismiss();
|
||||
// This dismisses the shade on opening the activity
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
|
||||
}
|
||||
|
||||
// for testing, to compare two CharSequences containing VpnSpans
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
return object instanceof VpnSpan;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 314159257; // prime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,793 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_CA_CERT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NETWORK;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TITLE;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_CA_CERT_SUBTITLE;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_NETWORK_SUBTITLE;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_VPN_SUBTITLE;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_NAMED_MANAGEMENT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_VIEW_POLICIES;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_CA_CERT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NETWORK;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MONITORING;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MULTIPLE_VPNS;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MONITORING;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_WORK_PROFILE_MONITORING;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_PERSONAL_PROFILE_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_MONITORING;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NAMED_VPN;
|
||||
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NETWORK;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.admin.DeviceAdminInfo;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.UserManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.internal.jank.InteractionJankMonitor;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.DialogCuj;
|
||||
import com.android.systemui.animation.DialogLaunchAnimator;
|
||||
import com.android.systemui.common.shared.model.Icon;
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
import com.android.systemui.dagger.qualifiers.Application;
|
||||
import com.android.systemui.dagger.qualifiers.Background;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig;
|
||||
import com.android.systemui.security.data.model.SecurityModel;
|
||||
import com.android.systemui.settings.UserTracker;
|
||||
import com.android.systemui.statusbar.phone.SystemUIDialog;
|
||||
import com.android.systemui.statusbar.policy.SecurityController;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Helper class for the configuration of the QS security footer button. */
|
||||
@SysUISingleton
|
||||
public class QSSecurityFooterUtils implements DialogInterface.OnClickListener {
|
||||
protected static final String TAG = "QSSecurityFooterUtils";
|
||||
protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
private static final boolean DEBUG_FORCE_VISIBLE = false;
|
||||
|
||||
private static final String INTERACTION_JANK_TAG = "managed_device_info";
|
||||
|
||||
@Application private Context mContext;
|
||||
private final DevicePolicyManager mDpm;
|
||||
|
||||
private final SecurityController mSecurityController;
|
||||
private final ActivityStarter mActivityStarter;
|
||||
private final Handler mMainHandler;
|
||||
private final UserTracker mUserTracker;
|
||||
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
||||
|
||||
private final AtomicBoolean mShouldUseSettingsButton = new AtomicBoolean(false);
|
||||
|
||||
protected Handler mBgHandler;
|
||||
private AlertDialog mDialog;
|
||||
|
||||
private Supplier<String> mManagementTitleSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(R.string.monitoring_title_device_owned);
|
||||
|
||||
private Supplier<String> mManagementMessageSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_management);
|
||||
|
||||
private Supplier<String> mManagementMonitoringStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_management_monitoring);
|
||||
|
||||
private Supplier<String> mManagementMultipleVpnStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_management_vpns);
|
||||
|
||||
private Supplier<String> mWorkProfileMonitoringStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_managed_profile_monitoring);
|
||||
|
||||
private Supplier<String> mWorkProfileNetworkStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.quick_settings_disclosure_managed_profile_network_activity);
|
||||
|
||||
private Supplier<String> mMonitoringSubtitleCaCertStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_subtitle_ca_certificate);
|
||||
|
||||
private Supplier<String> mMonitoringSubtitleNetworkStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_subtitle_network_logging);
|
||||
|
||||
private Supplier<String> mMonitoringSubtitleVpnStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(R.string.monitoring_subtitle_vpn);
|
||||
|
||||
private Supplier<String> mViewPoliciesButtonStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(R.string.monitoring_button_view_policies);
|
||||
|
||||
private Supplier<String> mManagementDialogStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_management);
|
||||
|
||||
private Supplier<String> mManagementDialogCaCertStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_management_ca_certificate);
|
||||
|
||||
private Supplier<String> mWorkProfileDialogCaCertStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_managed_profile_ca_certificate);
|
||||
|
||||
private Supplier<String> mManagementDialogNetworkStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_management_network_logging);
|
||||
|
||||
private Supplier<String> mWorkProfileDialogNetworkStringSupplier = () ->
|
||||
mContext == null ? null : mContext.getString(
|
||||
R.string.monitoring_description_managed_profile_network_logging);
|
||||
|
||||
@Inject
|
||||
QSSecurityFooterUtils(
|
||||
@Application Context context, DevicePolicyManager devicePolicyManager,
|
||||
UserTracker userTracker, @Main Handler mainHandler, ActivityStarter activityStarter,
|
||||
SecurityController securityController, @Background Looper bgLooper,
|
||||
DialogLaunchAnimator dialogLaunchAnimator) {
|
||||
mContext = context;
|
||||
mDpm = devicePolicyManager;
|
||||
mUserTracker = userTracker;
|
||||
mMainHandler = mainHandler;
|
||||
mActivityStarter = activityStarter;
|
||||
mSecurityController = securityController;
|
||||
mBgHandler = new Handler(bgLooper);
|
||||
mDialogLaunchAnimator = dialogLaunchAnimator;
|
||||
}
|
||||
|
||||
/** Show the device monitoring dialog. */
|
||||
public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) {
|
||||
createDialog(quickSettingsContext, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link SecurityButtonConfig} of the security button, or {@code null} if no
|
||||
* security button should be shown.
|
||||
*/
|
||||
@Nullable
|
||||
public SecurityButtonConfig getButtonConfig(SecurityModel securityModel) {
|
||||
final boolean isDeviceManaged = securityModel.isDeviceManaged();
|
||||
final UserInfo currentUser = mUserTracker.getUserInfo();
|
||||
final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null
|
||||
&& currentUser.isDemo();
|
||||
final boolean hasWorkProfile = securityModel.getHasWorkProfile();
|
||||
final boolean hasCACerts = securityModel.getHasCACertInCurrentUser();
|
||||
final boolean hasCACertsInWorkProfile = securityModel.getHasCACertInWorkProfile();
|
||||
final boolean isNetworkLoggingEnabled = securityModel.isNetworkLoggingEnabled();
|
||||
final String vpnName = securityModel.getPrimaryVpnName();
|
||||
final String vpnNameWorkProfile = securityModel.getWorkProfileVpnName();
|
||||
final CharSequence organizationName = securityModel.getDeviceOwnerOrganizationName();
|
||||
final CharSequence workProfileOrganizationName =
|
||||
securityModel.getWorkProfileOrganizationName();
|
||||
final boolean isProfileOwnerOfOrganizationOwnedDevice =
|
||||
securityModel.isProfileOwnerOfOrganizationOwnedDevice();
|
||||
final boolean isParentalControlsEnabled = securityModel.isParentalControlsEnabled();
|
||||
final boolean isWorkProfileOn = securityModel.isWorkProfileOn();
|
||||
final boolean hasDisclosableWorkProfilePolicy = hasCACertsInWorkProfile
|
||||
|| vpnNameWorkProfile != null || (hasWorkProfile && isNetworkLoggingEnabled);
|
||||
// Update visibility of footer
|
||||
boolean isVisible = (isDeviceManaged && !isDemoDevice)
|
||||
|| hasCACerts
|
||||
|| vpnName != null
|
||||
|| isProfileOwnerOfOrganizationOwnedDevice
|
||||
|| isParentalControlsEnabled
|
||||
|| (hasDisclosableWorkProfilePolicy && isWorkProfileOn);
|
||||
if (!isVisible && !DEBUG_FORCE_VISIBLE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the view to be untappable if the device is an organization-owned device with a
|
||||
// managed profile and there is either:
|
||||
// a) no policy set which requires a privacy disclosure.
|
||||
// b) a specific work policy set but the work profile is turned off.
|
||||
boolean isClickable = !(isProfileOwnerOfOrganizationOwnedDevice
|
||||
&& (!hasDisclosableWorkProfilePolicy || !isWorkProfileOn));
|
||||
|
||||
String text = getFooterText(isDeviceManaged, hasWorkProfile,
|
||||
hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName,
|
||||
vpnNameWorkProfile, organizationName, workProfileOrganizationName,
|
||||
isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled,
|
||||
isWorkProfileOn).toString();
|
||||
|
||||
Icon icon;
|
||||
if (isParentalControlsEnabled) {
|
||||
icon = new Icon.Loaded(securityModel.getDeviceAdminIcon());
|
||||
} else if (vpnName != null || vpnNameWorkProfile != null) {
|
||||
if (securityModel.isVpnBranded()) {
|
||||
icon = new Icon.Resource(R.drawable.stat_sys_branded_vpn);
|
||||
} else {
|
||||
icon = new Icon.Resource(R.drawable.stat_sys_vpn_ic);
|
||||
}
|
||||
} else {
|
||||
icon = new Icon.Resource(R.drawable.ic_info_outline);
|
||||
}
|
||||
|
||||
return new SecurityButtonConfig(icon, text, isClickable);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getFooterText(boolean isDeviceManaged, boolean hasWorkProfile,
|
||||
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
|
||||
String vpnName, String vpnNameWorkProfile, CharSequence organizationName,
|
||||
CharSequence workProfileOrganizationName,
|
||||
boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isParentalControlsEnabled,
|
||||
boolean isWorkProfileOn) {
|
||||
if (isParentalControlsEnabled) {
|
||||
return mContext.getString(R.string.quick_settings_disclosure_parental_controls);
|
||||
}
|
||||
if (isDeviceManaged || DEBUG_FORCE_VISIBLE) {
|
||||
return getManagedDeviceFooterText(hasCACerts, hasCACertsInWorkProfile,
|
||||
isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, organizationName);
|
||||
}
|
||||
return getManagedAndPersonalProfileFooterText(hasWorkProfile, hasCACerts,
|
||||
hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile,
|
||||
workProfileOrganizationName, isProfileOwnerOfOrganizationOwnedDevice,
|
||||
isWorkProfileOn);
|
||||
}
|
||||
|
||||
private String getManagedDeviceFooterText(
|
||||
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
|
||||
String vpnName, String vpnNameWorkProfile, CharSequence organizationName) {
|
||||
if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) {
|
||||
return getManagedDeviceMonitoringText(organizationName);
|
||||
}
|
||||
if (vpnName != null || vpnNameWorkProfile != null) {
|
||||
return getManagedDeviceVpnText(vpnName, vpnNameWorkProfile, organizationName);
|
||||
}
|
||||
return getMangedDeviceGeneralText(organizationName);
|
||||
}
|
||||
|
||||
private String getManagedDeviceMonitoringText(CharSequence organizationName) {
|
||||
if (organizationName == null) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_MANAGEMENT_MONITORING, mManagementMonitoringStringSupplier);
|
||||
}
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_MANAGEMENT_MONITORING,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_management_monitoring,
|
||||
organizationName),
|
||||
organizationName);
|
||||
}
|
||||
|
||||
private String getManagedDeviceVpnText(
|
||||
String vpnName, String vpnNameWorkProfile, CharSequence organizationName) {
|
||||
if (vpnName != null && vpnNameWorkProfile != null) {
|
||||
if (organizationName == null) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_MANAGEMENT_MULTIPLE_VPNS, mManagementMultipleVpnStringSupplier);
|
||||
}
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_management_vpns,
|
||||
organizationName),
|
||||
organizationName);
|
||||
}
|
||||
String name = vpnName != null ? vpnName : vpnNameWorkProfile;
|
||||
if (organizationName == null) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_MANAGEMENT_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_management_named_vpn,
|
||||
name),
|
||||
name);
|
||||
}
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_MANAGEMENT_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_management_named_vpn,
|
||||
organizationName,
|
||||
name),
|
||||
organizationName,
|
||||
name);
|
||||
}
|
||||
|
||||
private String getMangedDeviceGeneralText(CharSequence organizationName) {
|
||||
if (organizationName == null) {
|
||||
return mDpm.getResources().getString(QS_MSG_MANAGEMENT, mManagementMessageSupplier);
|
||||
}
|
||||
if (isFinancedDevice()) {
|
||||
return mContext.getString(
|
||||
R.string.quick_settings_financed_disclosure_named_management,
|
||||
organizationName);
|
||||
} else {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_MANAGEMENT,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_management,
|
||||
organizationName),
|
||||
organizationName);
|
||||
}
|
||||
}
|
||||
|
||||
private String getManagedAndPersonalProfileFooterText(boolean hasWorkProfile,
|
||||
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
|
||||
String vpnName, String vpnNameWorkProfile, CharSequence workProfileOrganizationName,
|
||||
boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isWorkProfileOn) {
|
||||
if (hasCACerts || (hasCACertsInWorkProfile && isWorkProfileOn)) {
|
||||
return getMonitoringText(
|
||||
hasCACerts, hasCACertsInWorkProfile, workProfileOrganizationName,
|
||||
isWorkProfileOn);
|
||||
}
|
||||
if (vpnName != null || (vpnNameWorkProfile != null && isWorkProfileOn)) {
|
||||
return getVpnText(hasWorkProfile, vpnName, vpnNameWorkProfile, isWorkProfileOn);
|
||||
}
|
||||
if (hasWorkProfile && isNetworkLoggingEnabled && isWorkProfileOn) {
|
||||
return getManagedProfileNetworkActivityText();
|
||||
}
|
||||
if (isProfileOwnerOfOrganizationOwnedDevice) {
|
||||
return getMangedDeviceGeneralText(workProfileOrganizationName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getMonitoringText(boolean hasCACerts, boolean hasCACertsInWorkProfile,
|
||||
CharSequence workProfileOrganizationName, boolean isWorkProfileOn) {
|
||||
if (hasCACertsInWorkProfile && isWorkProfileOn) {
|
||||
if (workProfileOrganizationName == null) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_WORK_PROFILE_MONITORING, mWorkProfileMonitoringStringSupplier);
|
||||
}
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_NAMED_WORK_PROFILE_MONITORING,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_named_managed_profile_monitoring,
|
||||
workProfileOrganizationName),
|
||||
workProfileOrganizationName);
|
||||
}
|
||||
if (hasCACerts) {
|
||||
return mContext.getString(R.string.quick_settings_disclosure_monitoring);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getVpnText(boolean hasWorkProfile, String vpnName, String vpnNameWorkProfile,
|
||||
boolean isWorkProfileOn) {
|
||||
if (vpnName != null && vpnNameWorkProfile != null) {
|
||||
return mContext.getString(R.string.quick_settings_disclosure_vpns);
|
||||
}
|
||||
if (vpnNameWorkProfile != null && isWorkProfileOn) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_WORK_PROFILE_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_managed_profile_named_vpn,
|
||||
vpnNameWorkProfile),
|
||||
vpnNameWorkProfile);
|
||||
}
|
||||
if (vpnName != null) {
|
||||
if (hasWorkProfile) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_PERSONAL_PROFILE_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.quick_settings_disclosure_personal_profile_named_vpn,
|
||||
vpnName),
|
||||
vpnName);
|
||||
}
|
||||
return mContext.getString(R.string.quick_settings_disclosure_named_vpn,
|
||||
vpnName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getManagedProfileNetworkActivityText() {
|
||||
return mDpm.getResources().getString(
|
||||
QS_MSG_WORK_PROFILE_NETWORK, mWorkProfileNetworkStringSupplier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (which == DialogInterface.BUTTON_NEGATIVE) {
|
||||
final Intent intent = new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS);
|
||||
dialog.dismiss();
|
||||
// This dismisses the shade on opening the activity
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void createDialog(Context quickSettingsContext, @Nullable View view) {
|
||||
mShouldUseSettingsButton.set(false);
|
||||
mBgHandler.post(() -> {
|
||||
String settingsButtonText = getSettingsButton();
|
||||
final View dialogView = createDialogView();
|
||||
mMainHandler.post(() -> {
|
||||
mDialog = new SystemUIDialog(quickSettingsContext, 0);
|
||||
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this);
|
||||
mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, mShouldUseSettingsButton.get()
|
||||
? settingsButtonText : getNegativeButton(), this);
|
||||
|
||||
mDialog.setView(dialogView);
|
||||
if (view != null && view.isAggregatedVisible()) {
|
||||
mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj(
|
||||
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG));
|
||||
} else {
|
||||
mDialog.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Dialog getDialog() {
|
||||
return mDialog;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
View createDialogView() {
|
||||
if (mSecurityController.isParentalControlsEnabled()) {
|
||||
return createParentalControlsDialogView();
|
||||
}
|
||||
return createOrganizationDialogView();
|
||||
}
|
||||
|
||||
private View createOrganizationDialogView() {
|
||||
final boolean isDeviceManaged = mSecurityController.isDeviceManaged();
|
||||
final boolean hasWorkProfile = mSecurityController.hasWorkProfile();
|
||||
final CharSequence deviceOwnerOrganization =
|
||||
mSecurityController.getDeviceOwnerOrganizationName();
|
||||
final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser();
|
||||
final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile();
|
||||
final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled();
|
||||
final String vpnName = mSecurityController.getPrimaryVpnName();
|
||||
final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName();
|
||||
|
||||
View dialogView = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.quick_settings_footer_dialog, null, false);
|
||||
|
||||
// device management section
|
||||
TextView deviceManagementSubtitle =
|
||||
dialogView.findViewById(R.id.device_management_subtitle);
|
||||
deviceManagementSubtitle.setText(getManagementTitle(deviceOwnerOrganization));
|
||||
|
||||
CharSequence managementMessage = getManagementMessage(isDeviceManaged,
|
||||
deviceOwnerOrganization);
|
||||
if (managementMessage == null) {
|
||||
dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.GONE);
|
||||
} else {
|
||||
dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.VISIBLE);
|
||||
TextView deviceManagementWarning =
|
||||
(TextView) dialogView.findViewById(R.id.device_management_warning);
|
||||
deviceManagementWarning.setText(managementMessage);
|
||||
mShouldUseSettingsButton.set(true);
|
||||
}
|
||||
|
||||
// ca certificate section
|
||||
CharSequence caCertsMessage = getCaCertsMessage(isDeviceManaged, hasCACerts,
|
||||
hasCACertsInWorkProfile);
|
||||
if (caCertsMessage == null) {
|
||||
dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.GONE);
|
||||
} else {
|
||||
dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.VISIBLE);
|
||||
TextView caCertsWarning = (TextView) dialogView.findViewById(R.id.ca_certs_warning);
|
||||
caCertsWarning.setText(caCertsMessage);
|
||||
// Make "Open trusted credentials"-link clickable
|
||||
caCertsWarning.setMovementMethod(new LinkMovementMethod());
|
||||
|
||||
TextView caCertsSubtitle = (TextView) dialogView.findViewById(R.id.ca_certs_subtitle);
|
||||
String caCertsSubtitleMessage = mDpm.getResources().getString(
|
||||
QS_DIALOG_MONITORING_CA_CERT_SUBTITLE, mMonitoringSubtitleCaCertStringSupplier);
|
||||
caCertsSubtitle.setText(caCertsSubtitleMessage);
|
||||
|
||||
}
|
||||
|
||||
// network logging section
|
||||
CharSequence networkLoggingMessage = getNetworkLoggingMessage(isDeviceManaged,
|
||||
isNetworkLoggingEnabled);
|
||||
if (networkLoggingMessage == null) {
|
||||
dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.GONE);
|
||||
} else {
|
||||
dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.VISIBLE);
|
||||
TextView networkLoggingWarning =
|
||||
(TextView) dialogView.findViewById(R.id.network_logging_warning);
|
||||
networkLoggingWarning.setText(networkLoggingMessage);
|
||||
|
||||
TextView networkLoggingSubtitle = (TextView) dialogView.findViewById(
|
||||
R.id.network_logging_subtitle);
|
||||
String networkLoggingSubtitleMessage = mDpm.getResources().getString(
|
||||
QS_DIALOG_MONITORING_NETWORK_SUBTITLE,
|
||||
mMonitoringSubtitleNetworkStringSupplier);
|
||||
networkLoggingSubtitle.setText(networkLoggingSubtitleMessage);
|
||||
}
|
||||
|
||||
// vpn section
|
||||
CharSequence vpnMessage = getVpnMessage(isDeviceManaged, hasWorkProfile, vpnName,
|
||||
vpnNameWorkProfile);
|
||||
if (vpnMessage == null) {
|
||||
dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.GONE);
|
||||
} else {
|
||||
dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.VISIBLE);
|
||||
TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning);
|
||||
vpnWarning.setText(vpnMessage);
|
||||
// Make "Open VPN Settings"-link clickable
|
||||
vpnWarning.setMovementMethod(new LinkMovementMethod());
|
||||
|
||||
TextView vpnSubtitle = (TextView) dialogView.findViewById(R.id.vpn_subtitle);
|
||||
String vpnSubtitleMessage = mDpm.getResources().getString(
|
||||
QS_DIALOG_MONITORING_VPN_SUBTITLE, mMonitoringSubtitleVpnStringSupplier);
|
||||
vpnSubtitle.setText(vpnSubtitleMessage);
|
||||
}
|
||||
|
||||
// Note: if a new section is added, should update configSubtitleVisibility to include
|
||||
// the handling of the subtitle
|
||||
configSubtitleVisibility(managementMessage != null,
|
||||
caCertsMessage != null,
|
||||
networkLoggingMessage != null,
|
||||
vpnMessage != null,
|
||||
dialogView);
|
||||
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
private View createParentalControlsDialogView() {
|
||||
View dialogView = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false);
|
||||
|
||||
DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo();
|
||||
Drawable icon = mSecurityController.getIcon(info);
|
||||
if (icon != null) {
|
||||
ImageView imageView = (ImageView) dialogView.findViewById(R.id.parental_controls_icon);
|
||||
imageView.setImageDrawable(icon);
|
||||
}
|
||||
|
||||
TextView parentalControlsTitle =
|
||||
(TextView) dialogView.findViewById(R.id.parental_controls_title);
|
||||
parentalControlsTitle.setText(mSecurityController.getLabel(info));
|
||||
|
||||
return dialogView;
|
||||
}
|
||||
|
||||
protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts,
|
||||
boolean showNetworkLogging, boolean showVpn, View dialogView) {
|
||||
// Device Management title should always been shown
|
||||
// When there is a Device Management message, all subtitles should be shown
|
||||
if (showDeviceManagement) {
|
||||
return;
|
||||
}
|
||||
// Hide the subtitle if there is only 1 message shown
|
||||
int mSectionCountExcludingDeviceMgt = 0;
|
||||
if (showCaCerts) {
|
||||
mSectionCountExcludingDeviceMgt++;
|
||||
}
|
||||
if (showNetworkLogging) {
|
||||
mSectionCountExcludingDeviceMgt++;
|
||||
}
|
||||
if (showVpn) {
|
||||
mSectionCountExcludingDeviceMgt++;
|
||||
}
|
||||
|
||||
// No work needed if there is no sections or more than 1 section
|
||||
if (mSectionCountExcludingDeviceMgt != 1) {
|
||||
return;
|
||||
}
|
||||
if (showCaCerts) {
|
||||
dialogView.findViewById(R.id.ca_certs_subtitle).setVisibility(View.GONE);
|
||||
}
|
||||
if (showNetworkLogging) {
|
||||
dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE);
|
||||
}
|
||||
if (showVpn) {
|
||||
dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// This should not be called on the main thread to avoid making an IPC.
|
||||
@VisibleForTesting
|
||||
String getSettingsButton() {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_VIEW_POLICIES, mViewPoliciesButtonStringSupplier);
|
||||
}
|
||||
|
||||
private String getPositiveButton() {
|
||||
return mContext.getString(R.string.ok);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getNegativeButton() {
|
||||
if (mSecurityController.isParentalControlsEnabled()) {
|
||||
return mContext.getString(R.string.monitoring_button_view_controls);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getManagementMessage(boolean isDeviceManaged,
|
||||
CharSequence organizationName) {
|
||||
if (!isDeviceManaged) {
|
||||
return null;
|
||||
}
|
||||
if (organizationName != null) {
|
||||
if (isFinancedDevice()) {
|
||||
return mContext.getString(R.string.monitoring_financed_description_named_management,
|
||||
organizationName, organizationName);
|
||||
} else {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_NAMED_MANAGEMENT,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_named_management,
|
||||
organizationName),
|
||||
organizationName);
|
||||
}
|
||||
}
|
||||
return mDpm.getResources().getString(QS_DIALOG_MANAGEMENT, mManagementDialogStringSupplier);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts,
|
||||
boolean hasCACertsInWorkProfile) {
|
||||
if (!(hasCACerts || hasCACertsInWorkProfile)) return null;
|
||||
if (isDeviceManaged) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_CA_CERT, mManagementDialogCaCertStringSupplier);
|
||||
}
|
||||
if (hasCACertsInWorkProfile) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_WORK_PROFILE_CA_CERT, mWorkProfileDialogCaCertStringSupplier);
|
||||
}
|
||||
return mContext.getString(R.string.monitoring_description_ca_certificate);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getNetworkLoggingMessage(boolean isDeviceManaged,
|
||||
boolean isNetworkLoggingEnabled) {
|
||||
if (!isNetworkLoggingEnabled) return null;
|
||||
if (isDeviceManaged) {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_NETWORK, mManagementDialogNetworkStringSupplier);
|
||||
} else {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_WORK_PROFILE_NETWORK, mWorkProfileDialogNetworkStringSupplier);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected CharSequence getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile,
|
||||
String vpnName, String vpnNameWorkProfile) {
|
||||
if (vpnName == null && vpnNameWorkProfile == null) return null;
|
||||
final SpannableStringBuilder message = new SpannableStringBuilder();
|
||||
if (isDeviceManaged) {
|
||||
if (vpnName != null && vpnNameWorkProfile != null) {
|
||||
String namedVpns = mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_two_named_vpns,
|
||||
vpnName, vpnNameWorkProfile),
|
||||
vpnName, vpnNameWorkProfile);
|
||||
message.append(namedVpns);
|
||||
} else {
|
||||
String name = vpnName != null ? vpnName : vpnNameWorkProfile;
|
||||
String namedVp = mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_NAMED_VPN,
|
||||
() -> mContext.getString(R.string.monitoring_description_named_vpn, name),
|
||||
name);
|
||||
message.append(namedVp);
|
||||
}
|
||||
} else {
|
||||
if (vpnName != null && vpnNameWorkProfile != null) {
|
||||
String namedVpns = mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_two_named_vpns,
|
||||
vpnName, vpnNameWorkProfile),
|
||||
vpnName, vpnNameWorkProfile);
|
||||
message.append(namedVpns);
|
||||
} else if (vpnNameWorkProfile != null) {
|
||||
String namedVpn = mDpm.getResources().getString(
|
||||
QS_DIALOG_WORK_PROFILE_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_managed_profile_named_vpn,
|
||||
vpnNameWorkProfile),
|
||||
vpnNameWorkProfile);
|
||||
message.append(namedVpn);
|
||||
} else if (hasWorkProfile) {
|
||||
String namedVpn = mDpm.getResources().getString(
|
||||
QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN,
|
||||
() -> mContext.getString(
|
||||
R.string.monitoring_description_personal_profile_named_vpn,
|
||||
vpnName),
|
||||
vpnName);
|
||||
message.append(namedVpn);
|
||||
} else {
|
||||
message.append(mContext.getString(R.string.monitoring_description_named_vpn,
|
||||
vpnName));
|
||||
}
|
||||
}
|
||||
message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator));
|
||||
message.append(mContext.getString(R.string.monitoring_description_vpn_settings),
|
||||
new VpnSpan(), 0);
|
||||
return message;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
CharSequence getManagementTitle(CharSequence deviceOwnerOrganization) {
|
||||
if (deviceOwnerOrganization != null && isFinancedDevice()) {
|
||||
return mContext.getString(R.string.monitoring_title_financed_device,
|
||||
deviceOwnerOrganization);
|
||||
} else {
|
||||
return mDpm.getResources().getString(
|
||||
QS_DIALOG_MANAGEMENT_TITLE,
|
||||
mManagementTitleSupplier);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFinancedDevice() {
|
||||
return mSecurityController.isDeviceManaged()
|
||||
&& mSecurityController.getDeviceOwnerType(
|
||||
mSecurityController.getDeviceOwnerComponentOnAnyUser())
|
||||
== DEVICE_OWNER_TYPE_FINANCED;
|
||||
}
|
||||
|
||||
protected class VpnSpan extends ClickableSpan {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS);
|
||||
mDialog.dismiss();
|
||||
// This dismisses the shade on opening the activity
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
|
||||
}
|
||||
|
||||
// for testing, to compare two CharSequences containing VpnSpans
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
return object instanceof VpnSpan;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 314159257; // prime
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.dagger
|
||||
|
||||
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
|
||||
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl
|
||||
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
|
||||
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
||||
/** Dagger module to provide/bind footer actions singletons. */
|
||||
@Module
|
||||
interface FooterActionsModule {
|
||||
@Binds fun userSwitcherRepository(impl: UserSwitcherRepositoryImpl): UserSwitcherRepository
|
||||
|
||||
@Binds
|
||||
fun foregroundServicesRepository(
|
||||
impl: ForegroundServicesRepositoryImpl
|
||||
): ForegroundServicesRepository
|
||||
|
||||
@Binds fun footerActionsInteractor(impl: FooterActionsInteractorImpl): FooterActionsInteractor
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.data.model
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
/** The current status of the User Switcher. */
|
||||
sealed class UserSwitcherStatusModel {
|
||||
/** The user switcher is disabled. */
|
||||
object Disabled : UserSwitcherStatusModel()
|
||||
|
||||
/** The user switcher is enabled. */
|
||||
data class Enabled(
|
||||
val currentUserName: String?,
|
||||
val currentUserImage: Drawable?,
|
||||
val isGuestUser: Boolean,
|
||||
) : UserSwitcherStatusModel()
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
|
||||
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.qs.FgsManagerController
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
|
||||
interface ForegroundServicesRepository {
|
||||
/**
|
||||
* The number of packages with a service running in the foreground.
|
||||
*
|
||||
* Note that this will be equal to 0 if [FgsManagerController.isAvailable] is false.
|
||||
*/
|
||||
val foregroundServicesCount: Flow<Int>
|
||||
|
||||
/**
|
||||
* Whether there were new changes to the foreground packages since a dialog was last shown.
|
||||
*
|
||||
* Note that this will be equal to `false` if [FgsManagerController.showFooterDot] is false.
|
||||
*/
|
||||
val hasNewChanges: Flow<Boolean>
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
class ForegroundServicesRepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
fgsManagerController: FgsManagerController,
|
||||
) : ForegroundServicesRepository {
|
||||
override val foregroundServicesCount: Flow<Int> =
|
||||
fgsManagerController.isAvailable
|
||||
.flatMapLatest { isAvailable ->
|
||||
if (!isAvailable) {
|
||||
return@flatMapLatest flowOf(0)
|
||||
}
|
||||
|
||||
conflatedCallbackFlow {
|
||||
fun updateState(numberOfPackages: Int) {
|
||||
trySendWithFailureLogging(numberOfPackages, TAG)
|
||||
}
|
||||
|
||||
val listener =
|
||||
object : FgsManagerController.OnNumberOfPackagesChangedListener {
|
||||
override fun onNumberOfPackagesChanged(numberOfPackages: Int) {
|
||||
updateState(numberOfPackages)
|
||||
}
|
||||
}
|
||||
|
||||
fgsManagerController.addOnNumberOfPackagesChangedListener(listener)
|
||||
updateState(fgsManagerController.numRunningPackages)
|
||||
awaitClose {
|
||||
fgsManagerController.removeOnNumberOfPackagesChangedListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val hasNewChanges: Flow<Boolean> =
|
||||
fgsManagerController.showFooterDot.flatMapLatest { showFooterDot ->
|
||||
if (!showFooterDot) {
|
||||
return@flatMapLatest flowOf(false)
|
||||
}
|
||||
|
||||
// A flow that emits whenever the FGS dialog is dismissed.
|
||||
val dialogDismissedEvents = conflatedCallbackFlow {
|
||||
fun updateState() {
|
||||
trySendWithFailureLogging(
|
||||
Unit,
|
||||
TAG,
|
||||
)
|
||||
}
|
||||
|
||||
val listener =
|
||||
object : FgsManagerController.OnDialogDismissedListener {
|
||||
override fun onDialogDismissed() {
|
||||
updateState()
|
||||
}
|
||||
}
|
||||
|
||||
fgsManagerController.addOnDialogDismissedListener(listener)
|
||||
awaitClose { fgsManagerController.removeOnDialogDismissedListener(listener) }
|
||||
}
|
||||
|
||||
// Query [fgsManagerController.newChangesSinceDialogWasDismissed] everytime the dialog
|
||||
// is dismissed or when [foregroundServices] is changing.
|
||||
merge(
|
||||
foregroundServicesCount,
|
||||
dialogDismissedEvents,
|
||||
)
|
||||
.map { fgsManagerController.newChangesSinceDialogWasDismissed }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ForegroundServicesRepositoryImpl"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Handler
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings.Global.USER_SWITCHER_ENABLED
|
||||
import com.android.keyguard.KeyguardUpdateMonitor
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
|
||||
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.qs.SettingObserver
|
||||
import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.statusbar.policy.UserInfoController
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||
import com.android.systemui.util.settings.GlobalSettings
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface UserSwitcherRepository {
|
||||
/** The current [UserSwitcherStatusModel]. */
|
||||
val userSwitcherStatus: Flow<UserSwitcherStatusModel>
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
class UserSwitcherRepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@Application private val context: Context,
|
||||
@Background private val bgHandler: Handler,
|
||||
@Background private val bgDispatcher: CoroutineDispatcher,
|
||||
private val userManager: UserManager,
|
||||
private val userTracker: UserTracker,
|
||||
private val userSwitcherController: UserSwitcherController,
|
||||
private val userInfoController: UserInfoController,
|
||||
private val globalSetting: GlobalSettings,
|
||||
) : UserSwitcherRepository {
|
||||
private val showUserSwitcherForSingleUser =
|
||||
context.resources.getBoolean(R.bool.qs_show_user_switcher_for_single_user)
|
||||
|
||||
/** Whether the user switcher is currently enabled. */
|
||||
private val isEnabled: Flow<Boolean> = conflatedCallbackFlow {
|
||||
suspend fun updateState() {
|
||||
trySendWithFailureLogging(isUserSwitcherEnabled(), TAG)
|
||||
}
|
||||
|
||||
val observer =
|
||||
object :
|
||||
SettingObserver(
|
||||
globalSetting,
|
||||
bgHandler,
|
||||
USER_SWITCHER_ENABLED,
|
||||
userTracker.userId,
|
||||
) {
|
||||
override fun handleValueChanged(value: Int, observedChange: Boolean) {
|
||||
if (observedChange) {
|
||||
launch { updateState() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observer.isListening = true
|
||||
updateState()
|
||||
awaitClose { observer.isListening = false }
|
||||
}
|
||||
|
||||
/** The current user name. */
|
||||
private val currentUserName: Flow<String?> = conflatedCallbackFlow {
|
||||
suspend fun updateState() {
|
||||
trySendWithFailureLogging(getCurrentUser(), TAG)
|
||||
}
|
||||
|
||||
val callback = UserSwitcherController.UserSwitchCallback { launch { updateState() } }
|
||||
|
||||
userSwitcherController.addUserSwitchCallback(callback)
|
||||
updateState()
|
||||
awaitClose { userSwitcherController.removeUserSwitchCallback(callback) }
|
||||
}
|
||||
|
||||
/** The current (icon, isGuestUser) values. */
|
||||
// TODO(b/242040009): Could we only use this callback to get the user name and remove
|
||||
// currentUsername above?
|
||||
private val currentUserInfo: Flow<Pair<Drawable?, Boolean>> = conflatedCallbackFlow {
|
||||
val listener =
|
||||
UserInfoController.OnUserInfoChangedListener { _, picture, _ ->
|
||||
launch { trySendWithFailureLogging(picture to isGuestUser(), TAG) }
|
||||
}
|
||||
|
||||
// This will automatically call the listener when attached, so no need to update the state
|
||||
// here.
|
||||
userInfoController.addCallback(listener)
|
||||
awaitClose { userInfoController.removeCallback(listener) }
|
||||
}
|
||||
|
||||
override val userSwitcherStatus: Flow<UserSwitcherStatusModel> =
|
||||
isEnabled
|
||||
.flatMapLatest { enabled ->
|
||||
if (enabled) {
|
||||
combine(currentUserName, currentUserInfo) { name, (icon, isGuest) ->
|
||||
UserSwitcherStatusModel.Enabled(name, icon, isGuest)
|
||||
}
|
||||
} else {
|
||||
flowOf(UserSwitcherStatusModel.Disabled)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
private suspend fun isUserSwitcherEnabled(): Boolean {
|
||||
return withContext(bgDispatcher) {
|
||||
userManager.isUserSwitcherEnabled(showUserSwitcherForSingleUser)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCurrentUser(): String? {
|
||||
return withContext(bgDispatcher) { userSwitcherController.currentUserName }
|
||||
}
|
||||
|
||||
private suspend fun isGuestUser(): Boolean {
|
||||
return withContext(bgDispatcher) {
|
||||
userManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "UserSwitcherRepositoryImpl"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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.domain.interactor
|
||||
|
||||
import android.app.admin.DevicePolicyEventLogger
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.UserHandle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import com.android.internal.jank.InteractionJankMonitor
|
||||
import com.android.internal.logging.MetricsLogger
|
||||
import com.android.internal.logging.UiEventLogger
|
||||
import com.android.internal.logging.nano.MetricsProto
|
||||
import com.android.internal.util.FrameworkStatsLog
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.animation.Expandable
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.flags.FeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.globalactions.GlobalActionsDialogLite
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.qs.FgsManagerController
|
||||
import com.android.systemui.qs.QSSecurityFooterUtils
|
||||
import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
|
||||
import com.android.systemui.qs.footer.data.repository.ForegroundServicesRepository
|
||||
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepository
|
||||
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
|
||||
import com.android.systemui.qs.user.UserSwitchDialogController
|
||||
import com.android.systemui.security.data.repository.SecurityRepository
|
||||
import com.android.systemui.statusbar.policy.DeviceProvisionedController
|
||||
import com.android.systemui.user.UserSwitcherActivity
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** Interactor for the footer actions business logic. */
|
||||
interface FooterActionsInteractor {
|
||||
/** The current [SecurityButtonConfig]. */
|
||||
val securityButtonConfig: Flow<SecurityButtonConfig?>
|
||||
|
||||
/** The number of packages with a service running in the foreground. */
|
||||
val foregroundServicesCount: Flow<Int>
|
||||
|
||||
/** Whether there are new packages with a service running in the foreground. */
|
||||
val hasNewForegroundServices: Flow<Boolean>
|
||||
|
||||
/** The current [UserSwitcherStatusModel]. */
|
||||
val userSwitcherStatus: Flow<UserSwitcherStatusModel>
|
||||
|
||||
/**
|
||||
* The flow emitting `Unit` whenever a request to show the device monitoring dialog is fired.
|
||||
*/
|
||||
val deviceMonitoringDialogRequests: Flow<Unit>
|
||||
|
||||
/**
|
||||
* Show the device monitoring dialog, expanded from [view].
|
||||
*
|
||||
* Important: [view] must be associated to the same [Context] as the [Quick Settings fragment]
|
||||
* [com.android.systemui.qs.QSFragment].
|
||||
*/
|
||||
// TODO(b/230830644): Replace view by Expandable interface.
|
||||
fun showDeviceMonitoringDialog(view: View)
|
||||
|
||||
/**
|
||||
* Show the device monitoring dialog.
|
||||
*
|
||||
* Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings
|
||||
* fragment][com.android.systemui.qs.QSFragment].
|
||||
*/
|
||||
// TODO(b/230830644): Replace view by Expandable interface.
|
||||
fun showDeviceMonitoringDialog(quickSettingsContext: Context)
|
||||
|
||||
/** Show the foreground services dialog. */
|
||||
// TODO(b/230830644): Replace view by Expandable interface.
|
||||
fun showForegroundServicesDialog(view: View)
|
||||
|
||||
/** Show the power menu dialog. */
|
||||
// TODO(b/230830644): Replace view by Expandable interface.
|
||||
fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View)
|
||||
|
||||
/** Show the settings. */
|
||||
fun showSettings(expandable: Expandable)
|
||||
|
||||
/** Show the user switcher. */
|
||||
// TODO(b/230830644): Replace view by Expandable interface.
|
||||
fun showUserSwitcher(view: View)
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
class FooterActionsInteractorImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val activityStarter: ActivityStarter,
|
||||
private val featureFlags: FeatureFlags,
|
||||
private val metricsLogger: MetricsLogger,
|
||||
private val uiEventLogger: UiEventLogger,
|
||||
private val deviceProvisionedController: DeviceProvisionedController,
|
||||
private val qsSecurityFooterUtils: QSSecurityFooterUtils,
|
||||
private val fgsManagerController: FgsManagerController,
|
||||
private val userSwitchDialogController: UserSwitchDialogController,
|
||||
securityRepository: SecurityRepository,
|
||||
foregroundServicesRepository: ForegroundServicesRepository,
|
||||
userSwitcherRepository: UserSwitcherRepository,
|
||||
broadcastDispatcher: BroadcastDispatcher,
|
||||
@Background bgDispatcher: CoroutineDispatcher,
|
||||
) : FooterActionsInteractor {
|
||||
override val securityButtonConfig: Flow<SecurityButtonConfig?> =
|
||||
securityRepository.security.map { security ->
|
||||
withContext(bgDispatcher) { qsSecurityFooterUtils.getButtonConfig(security) }
|
||||
}
|
||||
|
||||
override val foregroundServicesCount: Flow<Int> =
|
||||
foregroundServicesRepository.foregroundServicesCount
|
||||
|
||||
override val hasNewForegroundServices: Flow<Boolean> =
|
||||
foregroundServicesRepository.hasNewChanges
|
||||
|
||||
override val userSwitcherStatus: Flow<UserSwitcherStatusModel> =
|
||||
userSwitcherRepository.userSwitcherStatus
|
||||
|
||||
override val deviceMonitoringDialogRequests: Flow<Unit> =
|
||||
broadcastDispatcher.broadcastFlow(
|
||||
IntentFilter(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG),
|
||||
UserHandle.ALL,
|
||||
Context.RECEIVER_EXPORTED,
|
||||
null,
|
||||
)
|
||||
|
||||
override fun showDeviceMonitoringDialog(view: View) {
|
||||
qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view)
|
||||
DevicePolicyEventLogger.createEvent(
|
||||
FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
|
||||
)
|
||||
.write()
|
||||
}
|
||||
|
||||
override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
|
||||
qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null)
|
||||
}
|
||||
|
||||
override fun showForegroundServicesDialog(view: View) {
|
||||
fgsManagerController.showDialog(view)
|
||||
}
|
||||
|
||||
override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) {
|
||||
uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
|
||||
globalActionsDialogLite.showOrHideDialog(
|
||||
/* keyguardShowing= */ false,
|
||||
/* isDeviceProvisioned= */ true,
|
||||
view,
|
||||
)
|
||||
}
|
||||
|
||||
override fun showSettings(expandable: Expandable) {
|
||||
if (!deviceProvisionedController.isCurrentUserSetup) {
|
||||
// If user isn't setup just unlock the device and dump them back at SUW.
|
||||
activityStarter.postQSRunnableDismissingKeyguard {}
|
||||
return
|
||||
}
|
||||
|
||||
metricsLogger.action(MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH)
|
||||
activityStarter.startActivity(
|
||||
Intent(Settings.ACTION_SETTINGS),
|
||||
true /* dismissShade */,
|
||||
expandable.activityLaunchController(
|
||||
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun showUserSwitcher(view: View) {
|
||||
if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
|
||||
userSwitchDialogController.showDialog(view)
|
||||
return
|
||||
}
|
||||
|
||||
val intent =
|
||||
Intent(view.context, UserSwitcherActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
activityStarter.startActivity(
|
||||
intent,
|
||||
true /* dismissShade */,
|
||||
ActivityLaunchAnimator.Controller.fromView(view, null),
|
||||
true /* showOverlockscreenwhenlocked */,
|
||||
UserHandle.SYSTEM,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.domain.model
|
||||
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
|
||||
/** The config for the security button. */
|
||||
data class SecurityButtonConfig(
|
||||
val icon: Icon,
|
||||
val text: String,
|
||||
val isClickable: Boolean,
|
||||
)
|
||||
@@ -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.binder.ContentDescriptionViewBinder
|
||||
import com.android.systemui.common.ui.binder.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.viewmodel
|
||||
|
||||
import android.annotation.DrawableRes
|
||||
import android.view.View
|
||||
import com.android.systemui.common.shared.model.ContentDescription
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
|
||||
/**
|
||||
* A ViewModel for a simple footer actions button. This is used for the user switcher, settings and
|
||||
* power buttons.
|
||||
*/
|
||||
data class FooterActionsButtonViewModel(
|
||||
val icon: Icon,
|
||||
val iconTint: Int?,
|
||||
@DrawableRes val background: Int,
|
||||
val contentDescription: ContentDescription,
|
||||
// TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
|
||||
// or activity.
|
||||
val onClick: (View) -> Unit,
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.viewmodel
|
||||
|
||||
import android.view.View
|
||||
|
||||
/** A ViewModel for the foreground services button. */
|
||||
data class FooterActionsForegroundServicesButtonViewModel(
|
||||
val foregroundServicesCount: Int,
|
||||
val text: String,
|
||||
val displayText: Boolean,
|
||||
val hasNewChanges: Boolean,
|
||||
val onClick: (View) -> Unit,
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.viewmodel
|
||||
|
||||
import android.view.View
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
|
||||
/** A ViewModel for the security button. */
|
||||
data class FooterActionsSecurityButtonViewModel(
|
||||
val icon: Icon,
|
||||
val text: String,
|
||||
val onClick: ((View) -> Unit)?,
|
||||
)
|
||||
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* 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.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.android.settingslib.Utils
|
||||
import com.android.settingslib.drawable.UserIconDrawable
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.animation.Expandable
|
||||
import com.android.systemui.common.shared.model.ContentDescription
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.globalactions.GlobalActionsDialogLite
|
||||
import com.android.systemui.plugins.FalsingManager
|
||||
import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED
|
||||
import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
|
||||
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
|
||||
import com.android.systemui.util.icuMessageFormat
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Provider
|
||||
import kotlin.math.max
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
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. */
|
||||
class FooterActionsViewModel(
|
||||
@Application private val context: Context,
|
||||
private val footerActionsInteractor: FooterActionsInteractor,
|
||||
private val falsingManager: FalsingManager,
|
||||
private val globalActionsDialogLite: GlobalActionsDialogLite,
|
||||
showPowerButton: Boolean,
|
||||
) {
|
||||
/**
|
||||
* Whether the UI rendering this ViewModel should be visible. Note that even when this is false,
|
||||
* the UI should still participate to the layout it is included in (i.e. in the View world it
|
||||
* should be INVISIBLE, not GONE).
|
||||
*/
|
||||
private val _isVisible = MutableStateFlow(true)
|
||||
val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow()
|
||||
|
||||
/** The alpha the UI rendering this ViewModel should have. */
|
||||
private val _alpha = MutableStateFlow(1f)
|
||||
val alpha: StateFlow<Float> = _alpha.asStateFlow()
|
||||
|
||||
/** The alpha the background of the UI rendering this ViewModel should have. */
|
||||
private val _backgroundAlpha = MutableStateFlow(1f)
|
||||
val backgroundAlpha: StateFlow<Float> = _backgroundAlpha.asStateFlow()
|
||||
|
||||
/** 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,
|
||||
)
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
val text =
|
||||
icuMessageFormat(
|
||||
context.resources,
|
||||
R.string.fgs_manager_footer_label,
|
||||
foregroundServicesCount,
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
userSwitcherButton(userSwitcherStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/** The model for the settings button. */
|
||||
val settings: FooterActionsButtonViewModel =
|
||||
FooterActionsButtonViewModel(
|
||||
Icon.Resource(R.drawable.ic_settings),
|
||||
iconTint = null,
|
||||
R.drawable.qs_footer_action_circle,
|
||||
ContentDescription.Resource(R.string.accessibility_quick_settings_settings),
|
||||
this::onSettingsButtonClicked,
|
||||
)
|
||||
|
||||
/** The model for the power button. */
|
||||
val power: FooterActionsButtonViewModel? =
|
||||
if (showPowerButton) {
|
||||
FooterActionsButtonViewModel(
|
||||
Icon.Resource(android.R.drawable.ic_lock_power_off),
|
||||
iconTint =
|
||||
Utils.getColorAttrDefaultColor(
|
||||
context,
|
||||
com.android.internal.R.attr.textColorOnAccent,
|
||||
),
|
||||
R.drawable.qs_footer_action_circle_color,
|
||||
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu),
|
||||
this::onPowerButtonClicked,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
/** Called when the visibility of the UI rendering this model should be changed. */
|
||||
fun onVisibilityChangeRequested(visible: Boolean) {
|
||||
_isVisible.value = visible
|
||||
}
|
||||
|
||||
/** Called when the expansion of the Quick Settings changed. */
|
||||
fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) {
|
||||
if (isInSplitShade) {
|
||||
// In split shade, we want to fade in the background only at the very end (see
|
||||
// b/240563302).
|
||||
val delay = 0.99f
|
||||
_alpha.value = expansion
|
||||
_backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay)
|
||||
} else {
|
||||
// Only start fading in the footer actions when we are at least 90% expanded.
|
||||
val delay = 0.9f
|
||||
_alpha.value = max(0f, expansion - delay) / (1 - delay)
|
||||
_backgroundAlpha.value = 1f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe the device monitoring dialog requests and show the dialog accordingly. This function
|
||||
* will suspend indefinitely and will need to be cancelled to stop observing.
|
||||
*
|
||||
* Important: [quickSettingsContext] must be the [Context] associated to the [Quick Settings
|
||||
* fragment][com.android.systemui.qs.QSFragment], and the call to this function must be
|
||||
* cancelled when that fragment is destroyed.
|
||||
*/
|
||||
suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
|
||||
footerActionsInteractor.deviceMonitoringDialogRequests.collect {
|
||||
footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSecurityButtonClicked(view: View) {
|
||||
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return
|
||||
}
|
||||
|
||||
footerActionsInteractor.showDeviceMonitoringDialog(view)
|
||||
}
|
||||
|
||||
private fun onForegroundServiceButtonClicked(view: View) {
|
||||
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return
|
||||
}
|
||||
|
||||
footerActionsInteractor.showForegroundServicesDialog(view)
|
||||
}
|
||||
|
||||
private fun onUserSwitcherClicked(view: View) {
|
||||
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return
|
||||
}
|
||||
|
||||
footerActionsInteractor.showUserSwitcher(view)
|
||||
}
|
||||
|
||||
// TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
|
||||
// or activity.
|
||||
private fun onSettingsButtonClicked(view: View) {
|
||||
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return
|
||||
}
|
||||
|
||||
footerActionsInteractor.showSettings(Expandable.fromView(view))
|
||||
}
|
||||
|
||||
private fun onPowerButtonClicked(view: View) {
|
||||
if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
|
||||
return
|
||||
}
|
||||
|
||||
footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view)
|
||||
}
|
||||
|
||||
private fun userSwitcherButton(
|
||||
status: UserSwitcherStatusModel.Enabled
|
||||
): FooterActionsButtonViewModel {
|
||||
val icon = status.currentUserImage!!
|
||||
val iconTint =
|
||||
if (status.isGuestUser && icon !is UserIconDrawable) {
|
||||
Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return FooterActionsButtonViewModel(
|
||||
Icon.Loaded(icon),
|
||||
iconTint,
|
||||
R.drawable.qs_footer_action_circle,
|
||||
ContentDescription.Loaded(userSwitcherContentDescription(status.currentUserName)),
|
||||
this::onUserSwitcherClicked,
|
||||
)
|
||||
}
|
||||
|
||||
private fun userSwitcherContentDescription(currentUser: String?): String? {
|
||||
return currentUser?.let { user ->
|
||||
context.getString(R.string.accessibility_quick_settings_user, user)
|
||||
}
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
class Factory
|
||||
@Inject
|
||||
constructor(
|
||||
@Application private val context: Context,
|
||||
private val falsingManager: FalsingManager,
|
||||
private val footerActionsInteractor: FooterActionsInteractor,
|
||||
private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
|
||||
@Named(PM_LITE_ENABLED) private val showPowerButton: Boolean,
|
||||
) {
|
||||
/** Create a [FooterActionsViewModel] bound to the lifecycle of [lifecycleOwner]. */
|
||||
fun create(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
|
||||
val globalActionsDialogLite = globalActionsDialogLiteProvider.get()
|
||||
if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||
// This should usually not happen, but let's make sure we already destroy
|
||||
// globalActionsDialogLite.
|
||||
globalActionsDialogLite.destroy()
|
||||
} else {
|
||||
// Destroy globalActionsDialogLite when the lifecycle is destroyed.
|
||||
lifecycleOwner.lifecycle.addObserver(
|
||||
object : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
globalActionsDialogLite.destroy()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return FooterActionsViewModel(
|
||||
context,
|
||||
footerActionsInteractor,
|
||||
falsingManager,
|
||||
globalActionsDialogLite,
|
||||
showPowerButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FooterActionsViewModel"
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import com.android.settingslib.Utils
|
||||
import com.android.systemui.FontSizeUtils
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.animation.LaunchableView
|
||||
import com.android.systemui.animation.LaunchableViewDelegate
|
||||
import com.android.systemui.plugins.qs.QSIconView
|
||||
import com.android.systemui.plugins.qs.QSTile
|
||||
import com.android.systemui.plugins.qs.QSTile.BooleanState
|
||||
@@ -138,8 +139,11 @@ open class QSTileViewImpl @JvmOverloads constructor(
|
||||
private var lastStateDescription: CharSequence? = null
|
||||
private var tileState = false
|
||||
private var lastState = INVALID
|
||||
private var blockVisibilityChanges = false
|
||||
private var lastVisibility = View.VISIBLE
|
||||
private val launchableViewDelegate = LaunchableViewDelegate(
|
||||
this,
|
||||
superSetVisibility = { super.setVisibility(it) },
|
||||
superSetTransitionVisibility = { super.setTransitionVisibility(it) },
|
||||
)
|
||||
|
||||
private val locInScreen = IntArray(2)
|
||||
|
||||
@@ -343,33 +347,15 @@ open class QSTileViewImpl @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun setShouldBlockVisibilityChanges(block: Boolean) {
|
||||
blockVisibilityChanges = block
|
||||
|
||||
if (block) {
|
||||
lastVisibility = visibility
|
||||
} else {
|
||||
visibility = lastVisibility
|
||||
}
|
||||
launchableViewDelegate.setShouldBlockVisibilityChanges(block)
|
||||
}
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
if (blockVisibilityChanges) {
|
||||
lastVisibility = visibility
|
||||
return
|
||||
}
|
||||
|
||||
super.setVisibility(visibility)
|
||||
launchableViewDelegate.setVisibility(visibility)
|
||||
}
|
||||
|
||||
override fun setTransitionVisibility(visibility: Int) {
|
||||
if (blockVisibilityChanges) {
|
||||
// View.setTransitionVisibility just sets the visibility flag, so we don't have to save
|
||||
// the transition visibility separately from the normal visibility.
|
||||
lastVisibility = visibility
|
||||
return
|
||||
}
|
||||
|
||||
super.setTransitionVisibility(visibility)
|
||||
launchableViewDelegate.setTransitionVisibility(visibility)
|
||||
}
|
||||
|
||||
// Accessibility
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.security.data.model
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.statusbar.policy.SecurityController
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** The security info exposed by [com.android.systemui.statusbar.policy.SecurityController]. */
|
||||
// TODO(b/242040009): Consider splitting this model into smaller submodels.
|
||||
data class SecurityModel(
|
||||
val isDeviceManaged: Boolean,
|
||||
val hasWorkProfile: Boolean,
|
||||
val isWorkProfileOn: Boolean,
|
||||
val isProfileOwnerOfOrganizationOwnedDevice: Boolean,
|
||||
val deviceOwnerOrganizationName: String?,
|
||||
val workProfileOrganizationName: String?,
|
||||
val isNetworkLoggingEnabled: Boolean,
|
||||
val isVpnBranded: Boolean,
|
||||
val primaryVpnName: String?,
|
||||
val workProfileVpnName: String?,
|
||||
val hasCACertInCurrentUser: Boolean,
|
||||
val hasCACertInWorkProfile: Boolean,
|
||||
val isParentalControlsEnabled: Boolean,
|
||||
val deviceAdminIcon: Drawable?,
|
||||
) {
|
||||
companion object {
|
||||
/** Create a [SecurityModel] from the current [securityController] state. */
|
||||
suspend fun create(
|
||||
securityController: SecurityController,
|
||||
@Background bgDispatcher: CoroutineDispatcher,
|
||||
): SecurityModel {
|
||||
return withContext(bgDispatcher) { create(securityController) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [SecurityModel] from the current [securityController] state.
|
||||
*
|
||||
* Important: This method should be called from a background thread as this will do a lot of
|
||||
* binder calls.
|
||||
*/
|
||||
// TODO(b/242040009): Remove this.
|
||||
@JvmStatic
|
||||
fun create(securityController: SecurityController): SecurityModel {
|
||||
val deviceAdminInfo =
|
||||
if (securityController.isParentalControlsEnabled) {
|
||||
securityController.deviceAdminInfo
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return SecurityModel(
|
||||
isDeviceManaged = securityController.isDeviceManaged,
|
||||
hasWorkProfile = securityController.hasWorkProfile(),
|
||||
isWorkProfileOn = securityController.isWorkProfileOn,
|
||||
isProfileOwnerOfOrganizationOwnedDevice =
|
||||
securityController.isProfileOwnerOfOrganizationOwnedDevice,
|
||||
deviceOwnerOrganizationName =
|
||||
securityController.deviceOwnerOrganizationName?.toString(),
|
||||
workProfileOrganizationName =
|
||||
securityController.workProfileOrganizationName?.toString(),
|
||||
isNetworkLoggingEnabled = securityController.isNetworkLoggingEnabled,
|
||||
isVpnBranded = securityController.isVpnBranded,
|
||||
primaryVpnName = securityController.primaryVpnName,
|
||||
workProfileVpnName = securityController.workProfileVpnName,
|
||||
hasCACertInCurrentUser = securityController.hasCACertInCurrentUser(),
|
||||
hasCACertInWorkProfile = securityController.hasCACertInWorkProfile(),
|
||||
isParentalControlsEnabled = securityController.isParentalControlsEnabled,
|
||||
deviceAdminIcon = securityController.getIcon(deviceAdminInfo),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.security.data.repository
|
||||
|
||||
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
|
||||
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.security.data.model.SecurityModel
|
||||
import com.android.systemui.statusbar.policy.SecurityController
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface SecurityRepository {
|
||||
/** The current [SecurityModel]. */
|
||||
val security: Flow<SecurityModel>
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
class SecurityRepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val securityController: SecurityController,
|
||||
@Background private val bgDispatcher: CoroutineDispatcher,
|
||||
) : SecurityRepository {
|
||||
override val security: Flow<SecurityModel> = conflatedCallbackFlow {
|
||||
suspend fun updateState() {
|
||||
trySendWithFailureLogging(SecurityModel.create(securityController, bgDispatcher), TAG)
|
||||
}
|
||||
|
||||
val callback = SecurityController.SecurityControllerCallback { launch { updateState() } }
|
||||
|
||||
securityController.addCallback(callback)
|
||||
updateState()
|
||||
awaitClose { securityController.removeCallback(callback) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SecurityRepositoryImpl"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.security.data.repository
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
||||
/** Dagger module to provide/bind security repositories. */
|
||||
@Module
|
||||
interface SecurityRepositoryModule {
|
||||
@Binds fun securityRepository(impl: SecurityRepositoryImpl): SecurityRepository
|
||||
}
|
||||
@@ -20,12 +20,28 @@ import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.systemui.animation.LaunchableView;
|
||||
import com.android.systemui.animation.LaunchableViewDelegate;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* A frame layout which does not have overlapping renderings commands and therefore does not need a
|
||||
* layer when alpha is changed.
|
||||
*/
|
||||
public class AlphaOptimizedFrameLayout extends FrameLayout
|
||||
public class AlphaOptimizedFrameLayout extends FrameLayout implements LaunchableView
|
||||
{
|
||||
private final LaunchableViewDelegate mLaunchableViewDelegate = new LaunchableViewDelegate(
|
||||
this,
|
||||
visibility -> {
|
||||
super.setVisibility(visibility);
|
||||
return Unit.INSTANCE;
|
||||
},
|
||||
visibility -> {
|
||||
super.setTransitionVisibility(visibility);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
public AlphaOptimizedFrameLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -47,4 +63,19 @@ public class AlphaOptimizedFrameLayout extends FrameLayout
|
||||
public boolean hasOverlappingRendering() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShouldBlockVisibilityChanges(boolean block) {
|
||||
mLaunchableViewDelegate.setShouldBlockVisibilityChanges(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(int visibility) {
|
||||
mLaunchableViewDelegate.setVisibility(visibility);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransitionVisibility(int visibility) {
|
||||
mLaunchableViewDelegate.setTransitionVisibility(visibility);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,12 +10,10 @@ import android.graphics.Rect
|
||||
import android.os.Looper
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper.RunWithLooper
|
||||
import android.util.Log
|
||||
import android.view.IRemoteAnimationFinishedCallback
|
||||
import android.view.RemoteAnimationAdapter
|
||||
import android.view.RemoteAnimationTarget
|
||||
import android.view.SurfaceControl
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.test.filters.SmallTest
|
||||
@@ -51,7 +49,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
@Mock lateinit var listener: ActivityLaunchAnimator.Listener
|
||||
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
||||
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
|
||||
@Mock lateinit var failHandler: Log.TerribleFailureHandler
|
||||
|
||||
private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
|
||||
@get:Rule val rule = MockitoJUnit.rule()
|
||||
@@ -187,13 +184,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
||||
verify(controller).onLaunchAnimationStart(anyBoolean())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun controllerFromOrphanViewReturnsNullAndIsATerribleFailure() {
|
||||
Log.setWtfHandler(failHandler)
|
||||
assertNull(ActivityLaunchAnimator.Controller.fromView(View(mContext)))
|
||||
verify(failHandler).onTerribleFailure(any(), any(), anyBoolean())
|
||||
}
|
||||
|
||||
private fun fakeWindow(): RemoteAnimationTarget {
|
||||
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
|
||||
val taskInfo = ActivityManager.RunningTaskInfo()
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.systemui.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -32,22 +33,27 @@ import com.android.systemui.dump.DumpManager
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import java.util.concurrent.Executor
|
||||
import junit.framework.Assert.assertSame
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
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.Mockito.`when`
|
||||
import org.mockito.Mockito.anyInt
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
@@ -381,6 +387,39 @@ class BroadcastDispatcherTest : SysuiTestCase() {
|
||||
.clearPendingRemoval(broadcastReceiver, user1.identifier)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBroadcastFlow() = runBlockingTest {
|
||||
val flow = broadcastDispatcher.broadcastFlow(intentFilter, user1) { intent, receiver ->
|
||||
intent to receiver
|
||||
}
|
||||
|
||||
// Collect the values into collectedValues.
|
||||
val collectedValues = mutableListOf<Pair<Intent, BroadcastReceiver>>()
|
||||
val job = launch {
|
||||
flow.collect { collectedValues.add(it) }
|
||||
}
|
||||
|
||||
testableLooper.processAllMessages()
|
||||
verify(mockUBRUser1).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))
|
||||
val receiver = argumentCaptor.value.receiver
|
||||
|
||||
// Simulate fake broadcasted intents.
|
||||
val fakeIntents = listOf<Intent>(mock(), mock(), mock())
|
||||
fakeIntents.forEach { receiver.onReceive(mockContext, it) }
|
||||
|
||||
// The intents should have been collected.
|
||||
advanceUntilIdle()
|
||||
|
||||
val expectedValues = fakeIntents.map { it to receiver }
|
||||
assertThat(collectedValues).containsExactlyElementsIn(expectedValues)
|
||||
|
||||
// Stop the collection.
|
||||
job.cancel()
|
||||
|
||||
testableLooper.processAllMessages()
|
||||
verify(mockUBRUser1).unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
private fun setUserMock(mockContext: Context, user: UserHandle) {
|
||||
`when`(mockContext.user).thenReturn(user)
|
||||
`when`(mockContext.userId).thenReturn(user.identifier)
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.systemui.qs;
|
||||
|
||||
import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@@ -34,6 +36,7 @@ import android.content.pm.PackageManager;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.os.Binder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.DeviceConfig;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
@@ -185,9 +188,9 @@ public class FgsManagerControllerTest extends SysuiTestCase {
|
||||
public void testChangesSinceLastDialog() throws RemoteException {
|
||||
setUserProfiles(0);
|
||||
|
||||
Assert.assertFalse(mFmc.getChangesSinceDialog());
|
||||
Assert.assertFalse(mFmc.getNewChangesSinceDialogWasDismissed());
|
||||
mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg", 0, true);
|
||||
Assert.assertTrue(mFmc.getChangesSinceDialog());
|
||||
Assert.assertTrue(mFmc.getNewChangesSinceDialogWasDismissed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,7 +225,41 @@ public class FgsManagerControllerTest extends SysuiTestCase {
|
||||
Assert.assertEquals(2, mFmc.getNumRunningPackages());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testButtonVisibilityOnShowAllowlistButtonFlagChange() throws Exception {
|
||||
setUserProfiles(0);
|
||||
setBackgroundRestrictionExemptionReason("pkg", 12345, REASON_ALLOWLISTED_PACKAGE);
|
||||
|
||||
final Binder binder = new Binder();
|
||||
setShowStopButtonForUserAllowlistedApps(true);
|
||||
mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true);
|
||||
Assert.assertEquals(1, mFmc.visibleButtonsCount());
|
||||
|
||||
mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, false);
|
||||
Assert.assertEquals(0, mFmc.visibleButtonsCount());
|
||||
|
||||
setShowStopButtonForUserAllowlistedApps(false);
|
||||
mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true);
|
||||
Assert.assertEquals(0, mFmc.visibleButtonsCount());
|
||||
}
|
||||
|
||||
private void setShowStopButtonForUserAllowlistedApps(boolean enable) {
|
||||
mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||
SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
|
||||
enable ? "true" : "false", false);
|
||||
mBackgroundExecutor.advanceClockToLast();
|
||||
mBackgroundExecutor.runAllReady();
|
||||
}
|
||||
|
||||
private void setBackgroundRestrictionExemptionReason(String pkgName, int uid, int reason)
|
||||
throws Exception {
|
||||
Mockito.doReturn(uid)
|
||||
.when(mPackageManager)
|
||||
.getPackageUidAsUser(pkgName, UserHandle.getUserId(uid));
|
||||
Mockito.doReturn(reason)
|
||||
.when(mIActivityManager)
|
||||
.getBackgroundRestrictionExemptionReason(uid);
|
||||
}
|
||||
|
||||
FgsManagerController createFgsManagerController() throws RemoteException {
|
||||
ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor =
|
||||
@@ -232,7 +269,7 @@ public class FgsManagerControllerTest extends SysuiTestCase {
|
||||
ArgumentCaptor<BroadcastReceiver> showFgsManagerReceiverArgumentCaptor =
|
||||
ArgumentCaptor.forClass(BroadcastReceiver.class);
|
||||
|
||||
FgsManagerController result = new FgsManagerController(
|
||||
FgsManagerController result = new FgsManagerControllerImpl(
|
||||
mContext,
|
||||
mMainExecutor,
|
||||
mBackgroundExecutor,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -100,6 +100,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
private TextView mFooterText;
|
||||
private TestableImageView mPrimaryFooterIcon;
|
||||
private QSSecurityFooter mFooter;
|
||||
private QSSecurityFooterUtils mFooterUtils;
|
||||
@Mock
|
||||
private SecurityController mSecurityController;
|
||||
@Mock
|
||||
@@ -118,13 +119,16 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mTestableLooper = TestableLooper.get(this);
|
||||
Looper looper = mTestableLooper.getLooper();
|
||||
Handler mainHandler = new Handler(looper);
|
||||
when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class));
|
||||
mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext)
|
||||
.replace("ImageView", TestableImageView.class)
|
||||
.build().inflate(R.layout.quick_settings_security_footer, null, false);
|
||||
mFooter = new QSSecurityFooter(mRootView, mUserTracker, new Handler(looper),
|
||||
mActivityStarter, mSecurityController, mDialogLaunchAnimator, looper,
|
||||
mBroadcastDispatcher);
|
||||
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);
|
||||
mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon);
|
||||
|
||||
@@ -520,7 +524,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
when(mSecurityController.isDeviceManaged()).thenReturn(true);
|
||||
|
||||
assertEquals(mContext.getString(R.string.monitoring_title_device_owned),
|
||||
mFooter.getManagementTitle(MANAGING_ORGANIZATION));
|
||||
mFooterUtils.getManagementTitle(MANAGING_ORGANIZATION));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -531,12 +535,12 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
|
||||
assertEquals(mContext.getString(R.string.monitoring_title_financed_device,
|
||||
MANAGING_ORGANIZATION),
|
||||
mFooter.getManagementTitle(MANAGING_ORGANIZATION));
|
||||
mFooterUtils.getManagementTitle(MANAGING_ORGANIZATION));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetManagementMessage_noManagement() {
|
||||
assertEquals(null, mFooter.getManagementMessage(
|
||||
assertEquals(null, mFooterUtils.getManagementMessage(
|
||||
/* isDeviceManaged= */ false, MANAGING_ORGANIZATION));
|
||||
}
|
||||
|
||||
@@ -544,10 +548,10 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
public void testGetManagementMessage_deviceOwner() {
|
||||
assertEquals(mContext.getString(R.string.monitoring_description_named_management,
|
||||
MANAGING_ORGANIZATION),
|
||||
mFooter.getManagementMessage(
|
||||
mFooterUtils.getManagementMessage(
|
||||
/* isDeviceManaged= */ true, MANAGING_ORGANIZATION));
|
||||
assertEquals(mContext.getString(R.string.monitoring_description_management),
|
||||
mFooter.getManagementMessage(
|
||||
mFooterUtils.getManagementMessage(
|
||||
/* isDeviceManaged= */ true,
|
||||
/* organizationName= */ null));
|
||||
}
|
||||
@@ -560,68 +564,68 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
|
||||
assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management,
|
||||
MANAGING_ORGANIZATION, MANAGING_ORGANIZATION),
|
||||
mFooter.getManagementMessage(
|
||||
mFooterUtils.getManagementMessage(
|
||||
/* isDeviceManaged= */ true, MANAGING_ORGANIZATION));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCaCertsMessage() {
|
||||
assertEquals(null, mFooter.getCaCertsMessage(true, false, false));
|
||||
assertEquals(null, mFooter.getCaCertsMessage(false, false, false));
|
||||
assertEquals(null, mFooterUtils.getCaCertsMessage(true, false, false));
|
||||
assertEquals(null, mFooterUtils.getCaCertsMessage(false, false, false));
|
||||
assertEquals(mContext.getString(R.string.monitoring_description_management_ca_certificate),
|
||||
mFooter.getCaCertsMessage(true, true, true));
|
||||
mFooterUtils.getCaCertsMessage(true, true, true));
|
||||
assertEquals(mContext.getString(R.string.monitoring_description_management_ca_certificate),
|
||||
mFooter.getCaCertsMessage(true, false, true));
|
||||
mFooterUtils.getCaCertsMessage(true, false, true));
|
||||
assertEquals(mContext.getString(
|
||||
R.string.monitoring_description_managed_profile_ca_certificate),
|
||||
mFooter.getCaCertsMessage(false, false, true));
|
||||
mFooterUtils.getCaCertsMessage(false, false, true));
|
||||
assertEquals(mContext.getString(
|
||||
R.string.monitoring_description_ca_certificate),
|
||||
mFooter.getCaCertsMessage(false, true, false));
|
||||
mFooterUtils.getCaCertsMessage(false, true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNetworkLoggingMessage() {
|
||||
// Test network logging message on a device with a device owner.
|
||||
// Network traffic may be monitored on the device.
|
||||
assertEquals(null, mFooter.getNetworkLoggingMessage(true, false));
|
||||
assertEquals(null, mFooterUtils.getNetworkLoggingMessage(true, false));
|
||||
assertEquals(mContext.getString(R.string.monitoring_description_management_network_logging),
|
||||
mFooter.getNetworkLoggingMessage(true, true));
|
||||
mFooterUtils.getNetworkLoggingMessage(true, true));
|
||||
|
||||
// Test network logging message on a device with a managed profile owner
|
||||
// Network traffic may be monitored on the work profile.
|
||||
assertEquals(null, mFooter.getNetworkLoggingMessage(false, false));
|
||||
assertEquals(null, mFooterUtils.getNetworkLoggingMessage(false, false));
|
||||
assertEquals(
|
||||
mContext.getString(R.string.monitoring_description_managed_profile_network_logging),
|
||||
mFooter.getNetworkLoggingMessage(false, true));
|
||||
mFooterUtils.getNetworkLoggingMessage(false, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetVpnMessage() {
|
||||
assertEquals(null, mFooter.getVpnMessage(true, true, null, null));
|
||||
assertEquals(null, mFooterUtils.getVpnMessage(true, true, null, null));
|
||||
assertEquals(addLink(mContext.getString(R.string.monitoring_description_two_named_vpns,
|
||||
VPN_PACKAGE, VPN_PACKAGE_2)),
|
||||
mFooter.getVpnMessage(true, true, VPN_PACKAGE, VPN_PACKAGE_2));
|
||||
mFooterUtils.getVpnMessage(true, true, VPN_PACKAGE, VPN_PACKAGE_2));
|
||||
assertEquals(addLink(mContext.getString(R.string.monitoring_description_two_named_vpns,
|
||||
VPN_PACKAGE, VPN_PACKAGE_2)),
|
||||
mFooter.getVpnMessage(false, true, VPN_PACKAGE, VPN_PACKAGE_2));
|
||||
mFooterUtils.getVpnMessage(false, true, VPN_PACKAGE, VPN_PACKAGE_2));
|
||||
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
|
||||
VPN_PACKAGE)),
|
||||
mFooter.getVpnMessage(true, false, VPN_PACKAGE, null));
|
||||
mFooterUtils.getVpnMessage(true, false, VPN_PACKAGE, null));
|
||||
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
|
||||
VPN_PACKAGE)),
|
||||
mFooter.getVpnMessage(false, false, VPN_PACKAGE, null));
|
||||
mFooterUtils.getVpnMessage(false, false, VPN_PACKAGE, null));
|
||||
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
|
||||
VPN_PACKAGE_2)),
|
||||
mFooter.getVpnMessage(true, true, null, VPN_PACKAGE_2));
|
||||
mFooterUtils.getVpnMessage(true, true, null, VPN_PACKAGE_2));
|
||||
assertEquals(addLink(mContext.getString(
|
||||
R.string.monitoring_description_managed_profile_named_vpn,
|
||||
VPN_PACKAGE_2)),
|
||||
mFooter.getVpnMessage(false, true, null, VPN_PACKAGE_2));
|
||||
mFooterUtils.getVpnMessage(false, true, null, VPN_PACKAGE_2));
|
||||
assertEquals(addLink(mContext.getString(
|
||||
R.string.monitoring_description_personal_profile_named_vpn,
|
||||
VPN_PACKAGE)),
|
||||
mFooter.getVpnMessage(false, true, VPN_PACKAGE, null));
|
||||
mFooterUtils.getVpnMessage(false, true, VPN_PACKAGE, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -631,19 +635,19 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
|
||||
// Device Management subtitle should be shown when there is Device Management section only
|
||||
// Other sections visibility will be set somewhere else so it will not be tested here
|
||||
mFooter.configSubtitleVisibility(true, false, false, false, view);
|
||||
mFooterUtils.configSubtitleVisibility(true, false, false, false, view);
|
||||
assertEquals(View.VISIBLE,
|
||||
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
||||
|
||||
// If there are multiple sections, all subtitles should be shown
|
||||
mFooter.configSubtitleVisibility(true, true, false, false, view);
|
||||
mFooterUtils.configSubtitleVisibility(true, true, false, false, view);
|
||||
assertEquals(View.VISIBLE,
|
||||
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
||||
assertEquals(View.VISIBLE,
|
||||
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
|
||||
|
||||
// If there are multiple sections, all subtitles should be shown
|
||||
mFooter.configSubtitleVisibility(true, true, true, true, view);
|
||||
mFooterUtils.configSubtitleVisibility(true, true, true, true, view);
|
||||
assertEquals(View.VISIBLE,
|
||||
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
||||
assertEquals(View.VISIBLE,
|
||||
@@ -655,7 +659,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
|
||||
// If there are multiple sections, all subtitles should be shown, event if there is no
|
||||
// Device Management section
|
||||
mFooter.configSubtitleVisibility(false, true, true, true, view);
|
||||
mFooterUtils.configSubtitleVisibility(false, true, true, true, view);
|
||||
assertEquals(View.VISIBLE,
|
||||
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
|
||||
assertEquals(View.VISIBLE,
|
||||
@@ -664,13 +668,13 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
view.findViewById(R.id.vpn_subtitle).getVisibility());
|
||||
|
||||
// If there is only 1 section, the title should be hidden
|
||||
mFooter.configSubtitleVisibility(false, true, false, false, view);
|
||||
mFooterUtils.configSubtitleVisibility(false, true, false, false, view);
|
||||
assertEquals(View.GONE,
|
||||
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
|
||||
mFooter.configSubtitleVisibility(false, false, true, false, view);
|
||||
mFooterUtils.configSubtitleVisibility(false, false, true, false, view);
|
||||
assertEquals(View.GONE,
|
||||
view.findViewById(R.id.network_logging_subtitle).getVisibility());
|
||||
mFooter.configSubtitleVisibility(false, false, false, true, view);
|
||||
mFooterUtils.configSubtitleVisibility(false, false, false, true, view);
|
||||
assertEquals(View.GONE,
|
||||
view.findViewById(R.id.vpn_subtitle).getVisibility());
|
||||
}
|
||||
@@ -690,6 +694,9 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
|
||||
@Test
|
||||
public void testParentalControls() {
|
||||
// Make sure the security footer is visible, so that the images are updated.
|
||||
when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true);
|
||||
|
||||
when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
|
||||
|
||||
Drawable testDrawable = new VectorDrawable();
|
||||
@@ -719,7 +726,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
|
||||
when(mSecurityController.getLabel(any())).thenReturn(PARENTAL_CONTROLS_LABEL);
|
||||
|
||||
View view = mFooter.createDialogView();
|
||||
View view = mFooterUtils.createDialogView();
|
||||
TextView textView = (TextView) view.findViewById(R.id.parental_controls_title);
|
||||
assertEquals(PARENTAL_CONTROLS_LABEL, textView.getText());
|
||||
}
|
||||
@@ -742,7 +749,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
|
||||
.thenReturn(DEVICE_OWNER_TYPE_FINANCED);
|
||||
|
||||
View view = mFooter.createDialogView();
|
||||
View view = mFooterUtils.createDialogView();
|
||||
|
||||
TextView managementSubtitle = view.findViewById(R.id.device_management_subtitle);
|
||||
assertEquals(View.VISIBLE, managementSubtitle.getVisibility());
|
||||
@@ -753,7 +760,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management,
|
||||
MANAGING_ORGANIZATION, MANAGING_ORGANIZATION), managementMessage.getText());
|
||||
assertEquals(mContext.getString(R.string.monitoring_button_view_policies),
|
||||
mFooter.getSettingsButton());
|
||||
mFooterUtils.getSettingsButton());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -773,7 +780,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
AlertDialog dialog = dialogCaptor.getValue();
|
||||
dialog.create();
|
||||
|
||||
assertEquals(mFooter.getSettingsButton(),
|
||||
assertEquals(mFooterUtils.getSettingsButton(),
|
||||
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getText());
|
||||
|
||||
dialog.dismiss();
|
||||
@@ -816,8 +823,8 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG));
|
||||
mTestableLooper.processAllMessages();
|
||||
|
||||
assertTrue(mFooter.getDialog().isShowing());
|
||||
mFooter.getDialog().dismiss();
|
||||
assertTrue(mFooterUtils.getDialog().isShowing());
|
||||
mFooterUtils.getDialog().dismiss();
|
||||
}
|
||||
|
||||
private CharSequence addLink(CharSequence description) {
|
||||
@@ -825,7 +832,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
||||
message.append(description);
|
||||
message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator));
|
||||
message.append(mContext.getString(R.string.monitoring_description_vpn_settings),
|
||||
mFooter.new VpnSpan(), 0);
|
||||
mFooterUtils.new VpnSpan(), 0);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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.domain.interactor
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.UserHandle
|
||||
import android.provider.Settings
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import android.view.View
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.logging.nano.MetricsProto
|
||||
import com.android.internal.logging.testing.FakeMetricsLogger
|
||||
import com.android.internal.logging.testing.UiEventLoggerFake
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.globalactions.GlobalActionsDialogLite
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.qs.QSSecurityFooterUtils
|
||||
import com.android.systemui.qs.footer.FooterActionsTestUtils
|
||||
import com.android.systemui.qs.user.UserSwitchDialogController
|
||||
import com.android.systemui.statusbar.policy.DeviceProvisionedController
|
||||
import com.android.systemui.truth.correspondence.FakeUiEvent
|
||||
import com.android.systemui.truth.correspondence.LogMaker
|
||||
import com.android.systemui.user.UserSwitcherActivity
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.argumentCaptor
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.nullable
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
class FooterActionsInteractorTest : SysuiTestCase() {
|
||||
private lateinit var utils: FooterActionsTestUtils
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
utils = FooterActionsTestUtils(context, TestableLooper.get(this))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showDeviceMonitoringDialog() {
|
||||
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
|
||||
val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils)
|
||||
|
||||
val quickSettingsContext = mock<Context>()
|
||||
underTest.showDeviceMonitoringDialog(quickSettingsContext)
|
||||
verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
|
||||
|
||||
val view = mock<View>()
|
||||
whenever(view.context).thenReturn(quickSettingsContext)
|
||||
underTest.showDeviceMonitoringDialog(view)
|
||||
verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showPowerMenuDialog() {
|
||||
val uiEventLogger = UiEventLoggerFake()
|
||||
val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger)
|
||||
|
||||
val globalActionsDialogLite = mock<GlobalActionsDialogLite>()
|
||||
val view = mock<View>()
|
||||
underTest.showPowerMenuDialog(globalActionsDialogLite, view)
|
||||
|
||||
// Event is logged.
|
||||
val logs = uiEventLogger.logs
|
||||
assertThat(logs)
|
||||
.comparingElementsUsing(FakeUiEvent.EVENT_ID)
|
||||
.containsExactly(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS.id)
|
||||
|
||||
// Dialog is shown.
|
||||
verify(globalActionsDialogLite)
|
||||
.showOrHideDialog(
|
||||
/* keyguardShowing= */ false,
|
||||
/* isDeviceProvisioned= */ true,
|
||||
view,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showSettings_userSetUp() {
|
||||
val activityStarter = mock<ActivityStarter>()
|
||||
val deviceProvisionedController = mock<DeviceProvisionedController>()
|
||||
val metricsLogger = FakeMetricsLogger()
|
||||
|
||||
// User is set up.
|
||||
whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
|
||||
|
||||
val underTest =
|
||||
utils.footerActionsInteractor(
|
||||
activityStarter = activityStarter,
|
||||
deviceProvisionedController = deviceProvisionedController,
|
||||
metricsLogger = metricsLogger,
|
||||
)
|
||||
|
||||
underTest.showSettings(mock())
|
||||
|
||||
// Event is logged.
|
||||
assertThat(metricsLogger.logs.toList())
|
||||
.comparingElementsUsing(LogMaker.CATEGORY)
|
||||
.containsExactly(MetricsProto.MetricsEvent.ACTION_QS_EXPANDED_SETTINGS_LAUNCH)
|
||||
|
||||
// Activity is started.
|
||||
val intentCaptor = argumentCaptor<Intent>()
|
||||
verify(activityStarter)
|
||||
.startActivity(
|
||||
intentCaptor.capture(),
|
||||
/* dismissShade= */ eq(true),
|
||||
nullable() as? ActivityLaunchAnimator.Controller,
|
||||
)
|
||||
assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SETTINGS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showSettings_userNotSetUp() {
|
||||
val activityStarter = mock<ActivityStarter>()
|
||||
val deviceProvisionedController = mock<DeviceProvisionedController>()
|
||||
|
||||
// User is not set up.
|
||||
whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(false)
|
||||
|
||||
val underTest =
|
||||
utils.footerActionsInteractor(
|
||||
activityStarter = activityStarter,
|
||||
deviceProvisionedController = deviceProvisionedController,
|
||||
)
|
||||
|
||||
underTest.showSettings(mock())
|
||||
|
||||
// We only unlock the device.
|
||||
verify(activityStarter).postQSRunnableDismissingKeyguard(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showUserSwitcher_fullScreenDisabled() {
|
||||
val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
|
||||
val userSwitchDialogController = mock<UserSwitchDialogController>()
|
||||
val underTest =
|
||||
utils.footerActionsInteractor(
|
||||
featureFlags = featureFlags,
|
||||
userSwitchDialogController = userSwitchDialogController,
|
||||
)
|
||||
|
||||
val view = mock<View>()
|
||||
underTest.showUserSwitcher(view)
|
||||
|
||||
// Dialog is shown.
|
||||
verify(userSwitchDialogController).showDialog(view)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showUserSwitcher_fullScreenEnabled() {
|
||||
val featureFlags = FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) }
|
||||
val activityStarter = mock<ActivityStarter>()
|
||||
val underTest =
|
||||
utils.footerActionsInteractor(
|
||||
featureFlags = featureFlags,
|
||||
activityStarter = activityStarter,
|
||||
)
|
||||
|
||||
// The clicked view. The context is necessary because it's used to build the intent, that
|
||||
// we check below.
|
||||
val view = mock<View>()
|
||||
whenever(view.context).thenReturn(context)
|
||||
|
||||
underTest.showUserSwitcher(view)
|
||||
|
||||
// Dialog is shown.
|
||||
val intentCaptor = argumentCaptor<Intent>()
|
||||
verify(activityStarter)
|
||||
.startActivity(
|
||||
intentCaptor.capture(),
|
||||
/* dismissShade= */ eq(true),
|
||||
/* ActivityLaunchAnimator.Controller= */ nullable(),
|
||||
/* showOverLockscreenWhenLocked= */ eq(true),
|
||||
eq(UserHandle.SYSTEM),
|
||||
)
|
||||
assertThat(intentCaptor.value.component)
|
||||
.isEqualTo(
|
||||
ComponentName(
|
||||
context,
|
||||
UserSwitcherActivity::class.java,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
* 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.viewmodel
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.UserManager
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import android.testing.TestableLooper.RunWithLooper
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.settingslib.Utils
|
||||
import com.android.settingslib.drawable.UserIconDrawable
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.common.shared.model.ContentDescription
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
import com.android.systemui.qs.FakeFgsManagerController
|
||||
import com.android.systemui.qs.QSSecurityFooterUtils
|
||||
import com.android.systemui.qs.footer.FooterActionsTestUtils
|
||||
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
|
||||
import com.android.systemui.security.data.model.SecurityModel
|
||||
import com.android.systemui.settings.FakeUserTracker
|
||||
import com.android.systemui.statusbar.policy.FakeSecurityController
|
||||
import com.android.systemui.statusbar.policy.FakeUserInfoController
|
||||
import com.android.systemui.statusbar.policy.FakeUserInfoController.FakeInfo
|
||||
import com.android.systemui.statusbar.policy.MockUserSwitcherControllerWrapper
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.nullable
|
||||
import com.android.systemui.util.settings.FakeSettings
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.anyInt
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@RunWithLooper
|
||||
class FooterActionsViewModelTest : SysuiTestCase() {
|
||||
private lateinit var utils: FooterActionsTestUtils
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
utils = FooterActionsTestUtils(context, TestableLooper.get(this))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsButton() = runBlockingTest {
|
||||
val underTest = utils.footerActionsViewModel(showPowerButton = false)
|
||||
val settings = underTest.settings
|
||||
|
||||
assertThat(settings.contentDescription)
|
||||
.isEqualTo(ContentDescription.Resource(R.string.accessibility_quick_settings_settings))
|
||||
assertThat(settings.icon).isEqualTo(Icon.Resource(R.drawable.ic_settings))
|
||||
assertThat(settings.background).isEqualTo(R.drawable.qs_footer_action_circle)
|
||||
assertThat(settings.iconTint).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun powerButton() = runBlockingTest {
|
||||
// Without power button.
|
||||
val underTestWithoutPower = utils.footerActionsViewModel(showPowerButton = false)
|
||||
assertThat(underTestWithoutPower.power).isNull()
|
||||
|
||||
// With power button.
|
||||
val underTestWithPower = utils.footerActionsViewModel(showPowerButton = true)
|
||||
val power = underTestWithPower.power
|
||||
assertThat(power).isNotNull()
|
||||
assertThat(power!!.contentDescription)
|
||||
.isEqualTo(
|
||||
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
|
||||
)
|
||||
assertThat(power.icon).isEqualTo(Icon.Resource(android.R.drawable.ic_lock_power_off))
|
||||
assertThat(power.background).isEqualTo(R.drawable.qs_footer_action_circle_color)
|
||||
assertThat(power.iconTint)
|
||||
.isEqualTo(
|
||||
Utils.getColorAttrDefaultColor(
|
||||
context,
|
||||
com.android.internal.R.attr.textColorOnAccent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun userSwitcher() = runBlockingTest {
|
||||
val picture: Drawable = mock()
|
||||
val userInfoController = FakeUserInfoController(FakeInfo(picture = picture))
|
||||
val settings = FakeSettings()
|
||||
val userId = 42
|
||||
val userTracker = FakeUserTracker(userId)
|
||||
val userSwitcherControllerWrapper =
|
||||
MockUserSwitcherControllerWrapper(currentUserName = "foo")
|
||||
|
||||
// Mock UserManager.
|
||||
val userManager = mock<UserManager>()
|
||||
var isUserSwitcherEnabled = false
|
||||
var isGuestUser = false
|
||||
whenever(userManager.isUserSwitcherEnabled(any())).thenAnswer { isUserSwitcherEnabled }
|
||||
whenever(userManager.isGuestUser(any())).thenAnswer { isGuestUser }
|
||||
|
||||
val underTest =
|
||||
utils.footerActionsViewModel(
|
||||
showPowerButton = false,
|
||||
footerActionsInteractor =
|
||||
utils.footerActionsInteractor(
|
||||
userSwitcherRepository =
|
||||
utils.userSwitcherRepository(
|
||||
userTracker = userTracker,
|
||||
settings = settings,
|
||||
userManager = userManager,
|
||||
userInfoController = userInfoController,
|
||||
userSwitcherController = userSwitcherControllerWrapper.controller,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
// Collect the user switcher into currentUserSwitcher.
|
||||
var currentUserSwitcher: FooterActionsButtonViewModel? = null
|
||||
val job = launch { underTest.userSwitcher.collect { currentUserSwitcher = it } }
|
||||
fun currentUserSwitcher(): FooterActionsButtonViewModel? {
|
||||
// Make sure we finish collecting the current user switcher. This is necessary because
|
||||
// combined flows launch multiple coroutines in the current scope so we need to make
|
||||
// sure we process all coroutines triggered by our flow collection before we make
|
||||
// assertions on the current buttons.
|
||||
advanceUntilIdle()
|
||||
return currentUserSwitcher
|
||||
}
|
||||
|
||||
// The user switcher is disabled.
|
||||
assertThat(currentUserSwitcher()).isNull()
|
||||
|
||||
// Make the user manager return that the User Switcher is enabled. A change of the setting
|
||||
// for the current user will be fired to notify us of that change.
|
||||
isUserSwitcherEnabled = true
|
||||
|
||||
// Update the setting for a random user: nothing should change, given that at this point we
|
||||
// weren't notified of the change yet.
|
||||
utils.setUserSwitcherEnabled(settings, true, 3)
|
||||
assertThat(currentUserSwitcher()).isNull()
|
||||
|
||||
// Update the setting for the observed user: now we will be notified and the button should
|
||||
// be there.
|
||||
utils.setUserSwitcherEnabled(settings, true, userId)
|
||||
val userSwitcher = currentUserSwitcher()
|
||||
assertThat(userSwitcher).isNotNull()
|
||||
assertThat(userSwitcher!!.contentDescription)
|
||||
.isEqualTo(ContentDescription.Loaded("Signed in as foo"))
|
||||
assertThat(userSwitcher.icon).isEqualTo(Icon.Loaded(picture))
|
||||
assertThat(userSwitcher.background).isEqualTo(R.drawable.qs_footer_action_circle)
|
||||
|
||||
// Change the current user name.
|
||||
userSwitcherControllerWrapper.currentUserName = "bar"
|
||||
assertThat(currentUserSwitcher()?.contentDescription)
|
||||
.isEqualTo(ContentDescription.Loaded("Signed in as bar"))
|
||||
|
||||
fun iconTint(): Int? = currentUserSwitcher()!!.iconTint
|
||||
|
||||
// We tint the icon if the current user is not the guest.
|
||||
assertThat(iconTint()).isNull()
|
||||
|
||||
// Make the UserManager return that the current user is the guest. A change of the user
|
||||
// info will be fired to notify us of that change.
|
||||
isGuestUser = true
|
||||
|
||||
// At this point, there was no change of the user info yet so we still didn't pick the
|
||||
// UserManager change.
|
||||
assertThat(iconTint()).isNull()
|
||||
|
||||
// Trigger a user info change: there should now be a tint.
|
||||
userInfoController.updateInfo { userAccount = "doe" }
|
||||
assertThat(iconTint())
|
||||
.isEqualTo(
|
||||
Utils.getColorAttrDefaultColor(
|
||||
context,
|
||||
android.R.attr.colorForeground,
|
||||
)
|
||||
)
|
||||
|
||||
// Make sure we don't tint the icon if it is a user image (and not the default image), even
|
||||
// in guest mode.
|
||||
userInfoController.updateInfo { this.picture = mock<UserIconDrawable>() }
|
||||
assertThat(iconTint()).isNull()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun security() = runBlockingTest {
|
||||
val securityController = FakeSecurityController()
|
||||
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
|
||||
|
||||
// Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the
|
||||
// logic in securityToConfig.
|
||||
var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null }
|
||||
whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer {
|
||||
securityToConfig(it.arguments.first() as SecurityModel)
|
||||
}
|
||||
|
||||
val underTest =
|
||||
utils.footerActionsViewModel(
|
||||
footerActionsInteractor =
|
||||
utils.footerActionsInteractor(
|
||||
qsSecurityFooterUtils = qsSecurityFooterUtils,
|
||||
securityRepository =
|
||||
utils.securityRepository(
|
||||
securityController = securityController,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Collect the security model into currentSecurity.
|
||||
var currentSecurity: FooterActionsSecurityButtonViewModel? = null
|
||||
val job = launch { underTest.security.collect { currentSecurity = it } }
|
||||
fun currentSecurity(): FooterActionsSecurityButtonViewModel? {
|
||||
advanceUntilIdle()
|
||||
return currentSecurity
|
||||
}
|
||||
|
||||
// By default, we always return a null SecurityButtonConfig.
|
||||
assertThat(currentSecurity()).isNull()
|
||||
|
||||
// Map any SecurityModel into a non-null SecurityButtonConfig.
|
||||
val buttonConfig =
|
||||
SecurityButtonConfig(
|
||||
icon = Icon.Resource(0),
|
||||
text = "foo",
|
||||
isClickable = true,
|
||||
)
|
||||
securityToConfig = { buttonConfig }
|
||||
|
||||
// There was no change of the security info yet, so the mapper was not called yet.
|
||||
assertThat(currentSecurity()).isNull()
|
||||
|
||||
// Trigger a SecurityModel change, which will call the mapper and add a button.
|
||||
securityController.updateState {}
|
||||
var security = currentSecurity()
|
||||
assertThat(security).isNotNull()
|
||||
assertThat(security!!.icon).isEqualTo(buttonConfig.icon)
|
||||
assertThat(security.text).isEqualTo(buttonConfig.text)
|
||||
assertThat(security.onClick).isNotNull()
|
||||
|
||||
// If the config.clickable = false, then onClick should be null.
|
||||
securityToConfig = { buttonConfig.copy(isClickable = false) }
|
||||
securityController.updateState {}
|
||||
security = currentSecurity()
|
||||
assertThat(security).isNotNull()
|
||||
assertThat(security!!.onClick).isNull()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun foregroundServices() = runBlockingTest {
|
||||
val securityController = FakeSecurityController()
|
||||
val fgsManagerController =
|
||||
FakeFgsManagerController(
|
||||
isAvailable = true,
|
||||
showFooterDot = false,
|
||||
numRunningPackages = 0,
|
||||
)
|
||||
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
|
||||
|
||||
// Mock QSSecurityFooter to map a SecurityModel into a SecurityButtonConfig using the
|
||||
// logic in securityToConfig.
|
||||
var securityToConfig: (SecurityModel) -> SecurityButtonConfig? = { null }
|
||||
whenever(qsSecurityFooterUtils.getButtonConfig(any())).thenAnswer {
|
||||
securityToConfig(it.arguments.first() as SecurityModel)
|
||||
}
|
||||
|
||||
val underTest =
|
||||
utils.footerActionsViewModel(
|
||||
footerActionsInteractor =
|
||||
utils.footerActionsInteractor(
|
||||
qsSecurityFooterUtils = qsSecurityFooterUtils,
|
||||
securityRepository = utils.securityRepository(securityController),
|
||||
foregroundServicesRepository =
|
||||
utils.foregroundServicesRepository(fgsManagerController),
|
||||
),
|
||||
)
|
||||
|
||||
// Collect the security model into currentSecurity.
|
||||
var currentForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null
|
||||
val job = launch { underTest.foregroundServices.collect { currentForegroundServices = it } }
|
||||
fun currentForegroundServices(): FooterActionsForegroundServicesButtonViewModel? {
|
||||
advanceUntilIdle()
|
||||
return currentForegroundServices
|
||||
}
|
||||
|
||||
// We don't show the foreground services button if the number of running packages is not
|
||||
// > 1.
|
||||
assertThat(currentForegroundServices()).isNull()
|
||||
|
||||
// We show it at soon as the number of services is at least 1. Given that there is no
|
||||
// security, it should be displayed with text.
|
||||
fgsManagerController.numRunningPackages = 1
|
||||
val foregroundServices = currentForegroundServices()
|
||||
assertThat(foregroundServices).isNotNull()
|
||||
assertThat(foregroundServices!!.foregroundServicesCount).isEqualTo(1)
|
||||
assertThat(foregroundServices.text).isEqualTo("1 app is active")
|
||||
assertThat(foregroundServices.displayText).isTrue()
|
||||
assertThat(foregroundServices.onClick).isNotNull()
|
||||
|
||||
// We handle plurals correctly.
|
||||
fgsManagerController.numRunningPackages = 3
|
||||
assertThat(currentForegroundServices()?.text).isEqualTo("3 apps are active")
|
||||
|
||||
// Showing new changes (the footer dot) is currently disabled.
|
||||
assertThat(foregroundServices.hasNewChanges).isFalse()
|
||||
|
||||
// Enabling it will show the new changes.
|
||||
fgsManagerController.showFooterDot.value = true
|
||||
assertThat(currentForegroundServices()?.hasNewChanges).isTrue()
|
||||
|
||||
// Dismissing the dialog should remove the new changes dot.
|
||||
fgsManagerController.simulateDialogDismiss()
|
||||
assertThat(currentForegroundServices()?.hasNewChanges).isFalse()
|
||||
|
||||
// Showing the security button will make this show as a simple button without text.
|
||||
assertThat(foregroundServices.displayText).isTrue()
|
||||
securityToConfig = {
|
||||
SecurityButtonConfig(
|
||||
icon = Icon.Resource(0),
|
||||
text = "foo",
|
||||
isClickable = true,
|
||||
)
|
||||
}
|
||||
securityController.updateState {}
|
||||
assertThat(currentForegroundServices()?.displayText).isFalse()
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun observeDeviceMonitoringDialogRequests() = runBlockingTest {
|
||||
val qsSecurityFooterUtils = mock<QSSecurityFooterUtils>()
|
||||
val broadcastDispatcher = mock<BroadcastDispatcher>()
|
||||
|
||||
// Return a fake broadcastFlow that emits 3 fake events when collected.
|
||||
val broadcastFlow = flowOf(Unit, Unit, Unit)
|
||||
whenever(
|
||||
broadcastDispatcher.broadcastFlow(
|
||||
any(),
|
||||
nullable(),
|
||||
anyInt(),
|
||||
nullable(),
|
||||
)
|
||||
)
|
||||
.thenAnswer { broadcastFlow }
|
||||
|
||||
// Increment nDialogRequests whenever a request to show the dialog is made by the
|
||||
// FooterActionsInteractor.
|
||||
var nDialogRequests = 0
|
||||
whenever(qsSecurityFooterUtils.showDeviceMonitoringDialog(any(), nullable())).then {
|
||||
nDialogRequests++
|
||||
}
|
||||
|
||||
val underTest =
|
||||
utils.footerActionsViewModel(
|
||||
footerActionsInteractor =
|
||||
utils.footerActionsInteractor(
|
||||
qsSecurityFooterUtils = qsSecurityFooterUtils,
|
||||
broadcastDispatcher = broadcastDispatcher,
|
||||
),
|
||||
)
|
||||
|
||||
val job = launch {
|
||||
underTest.observeDeviceMonitoringDialogRequests(quickSettingsContext = mock())
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
assertThat(nDialogRequests).isEqualTo(3)
|
||||
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isVisible() {
|
||||
val underTest = utils.footerActionsViewModel()
|
||||
assertThat(underTest.isVisible.value).isTrue()
|
||||
|
||||
underTest.onVisibilityChangeRequested(visible = false)
|
||||
assertThat(underTest.isVisible.value).isFalse()
|
||||
|
||||
underTest.onVisibilityChangeRequested(visible = true)
|
||||
assertThat(underTest.isVisible.value).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,6 @@ class FakeFeatureFlags : FeatureFlags {
|
||||
stringFlags.put(flag.id, value)
|
||||
}
|
||||
|
||||
|
||||
override fun isEnabled(flag: UnreleasedFlag): Boolean = requireBooleanValue(flag.id)
|
||||
|
||||
override fun isEnabled(flag: ReleasedFlag): Boolean = requireBooleanValue(flag.id)
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 android.view.View
|
||||
import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener
|
||||
import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/** A fake [FgsManagerController] to be used in tests. */
|
||||
class FakeFgsManagerController(
|
||||
isAvailable: Boolean = true,
|
||||
showFooterDot: Boolean = false,
|
||||
numRunningPackages: Int = 0,
|
||||
) : FgsManagerController {
|
||||
override val isAvailable: MutableStateFlow<Boolean> = MutableStateFlow(isAvailable)
|
||||
|
||||
override var numRunningPackages = numRunningPackages
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
field = value
|
||||
newChangesSinceDialogWasDismissed = true
|
||||
numRunningPackagesListeners.forEach { it.onNumberOfPackagesChanged(value) }
|
||||
}
|
||||
}
|
||||
|
||||
override var newChangesSinceDialogWasDismissed = false
|
||||
private set
|
||||
|
||||
override val showFooterDot: MutableStateFlow<Boolean> = MutableStateFlow(showFooterDot)
|
||||
|
||||
private val numRunningPackagesListeners = LinkedHashSet<OnNumberOfPackagesChangedListener>()
|
||||
private val dialogDismissedListeners = LinkedHashSet<OnDialogDismissedListener>()
|
||||
|
||||
/** Simulate that a fgs dialog was just dismissed. */
|
||||
fun simulateDialogDismiss() {
|
||||
newChangesSinceDialogWasDismissed = false
|
||||
dialogDismissedListeners.forEach { it.onDialogDismissed() }
|
||||
}
|
||||
|
||||
override fun init() {}
|
||||
|
||||
override fun showDialog(viewLaunchedFrom: View?) {}
|
||||
|
||||
override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
|
||||
numRunningPackagesListeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeOnNumberOfPackagesChangedListener(
|
||||
listener: OnNumberOfPackagesChangedListener
|
||||
) {
|
||||
numRunningPackagesListeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun addOnDialogDismissedListener(listener: OnDialogDismissedListener) {
|
||||
dialogDismissedListeners.add(listener)
|
||||
}
|
||||
|
||||
override fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener) {
|
||||
dialogDismissedListeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun shouldUpdateFooterVisibility(): Boolean = false
|
||||
|
||||
override fun visibleButtonsCount(): Int = 0
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings
|
||||
import android.testing.TestableLooper
|
||||
import com.android.internal.logging.MetricsLogger
|
||||
import com.android.internal.logging.UiEventLogger
|
||||
import com.android.internal.logging.testing.FakeMetricsLogger
|
||||
import com.android.internal.logging.testing.UiEventLoggerFake
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.classifier.FalsingManagerFake
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.FeatureFlags
|
||||
import com.android.systemui.globalactions.GlobalActionsDialogLite
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.plugins.FalsingManager
|
||||
import com.android.systemui.qs.FakeFgsManagerController
|
||||
import com.android.systemui.qs.FgsManagerController
|
||||
import com.android.systemui.qs.QSSecurityFooterUtils
|
||||
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
|
||||
import com.android.systemui.qs.footer.data.repository.UserSwitcherRepositoryImpl
|
||||
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
|
||||
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl
|
||||
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
|
||||
import com.android.systemui.qs.user.UserSwitchDialogController
|
||||
import com.android.systemui.security.data.repository.SecurityRepository
|
||||
import com.android.systemui.security.data.repository.SecurityRepositoryImpl
|
||||
import com.android.systemui.settings.FakeUserTracker
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.statusbar.policy.DeviceProvisionedController
|
||||
import com.android.systemui.statusbar.policy.FakeSecurityController
|
||||
import com.android.systemui.statusbar.policy.FakeUserInfoController
|
||||
import com.android.systemui.statusbar.policy.SecurityController
|
||||
import com.android.systemui.statusbar.policy.UserInfoController
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.settings.FakeSettings
|
||||
import com.android.systemui.util.settings.GlobalSettings
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
|
||||
/**
|
||||
* Util class to create real implementations of the FooterActions repositories, viewModel and
|
||||
* interactor to be used in tests.
|
||||
*/
|
||||
class FooterActionsTestUtils(
|
||||
private val context: Context,
|
||||
private val testableLooper: TestableLooper,
|
||||
private val fakeClock: FakeSystemClock = FakeSystemClock(),
|
||||
) {
|
||||
/** Enable or disable the user switcher in the settings. */
|
||||
fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) {
|
||||
settings.putBoolForUser(Settings.Global.USER_SWITCHER_ENABLED, enabled, userId)
|
||||
|
||||
// The settings listener is processing messages on the bgHandler (usually backed by a
|
||||
// testableLooper in tests), so let's make sure we process the callback before continuing.
|
||||
testableLooper.processAllMessages()
|
||||
}
|
||||
|
||||
/** Create a [FooterActionsViewModel] to be used in tests. */
|
||||
fun footerActionsViewModel(
|
||||
@Application context: Context = this.context.applicationContext,
|
||||
footerActionsInteractor: FooterActionsInteractor = footerActionsInteractor(),
|
||||
falsingManager: FalsingManager = FalsingManagerFake(),
|
||||
globalActionsDialogLite: GlobalActionsDialogLite = mock(),
|
||||
showPowerButton: Boolean = true,
|
||||
): FooterActionsViewModel {
|
||||
return FooterActionsViewModel(
|
||||
context,
|
||||
footerActionsInteractor,
|
||||
falsingManager,
|
||||
globalActionsDialogLite,
|
||||
showPowerButton,
|
||||
)
|
||||
}
|
||||
|
||||
/** Create a [FooterActionsInteractor] to be used in tests. */
|
||||
fun footerActionsInteractor(
|
||||
activityStarter: ActivityStarter = mock(),
|
||||
featureFlags: FeatureFlags = FakeFeatureFlags(),
|
||||
metricsLogger: MetricsLogger = FakeMetricsLogger(),
|
||||
uiEventLogger: UiEventLogger = UiEventLoggerFake(),
|
||||
deviceProvisionedController: DeviceProvisionedController = mock(),
|
||||
qsSecurityFooterUtils: QSSecurityFooterUtils = mock(),
|
||||
fgsManagerController: FgsManagerController = mock(),
|
||||
userSwitchDialogController: UserSwitchDialogController = mock(),
|
||||
securityRepository: SecurityRepository = securityRepository(),
|
||||
foregroundServicesRepository: ForegroundServicesRepository = foregroundServicesRepository(),
|
||||
userSwitcherRepository: UserSwitcherRepository = userSwitcherRepository(),
|
||||
broadcastDispatcher: BroadcastDispatcher = mock(),
|
||||
bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(),
|
||||
): FooterActionsInteractor {
|
||||
return FooterActionsInteractorImpl(
|
||||
activityStarter,
|
||||
featureFlags,
|
||||
metricsLogger,
|
||||
uiEventLogger,
|
||||
deviceProvisionedController,
|
||||
qsSecurityFooterUtils,
|
||||
fgsManagerController,
|
||||
userSwitchDialogController,
|
||||
securityRepository,
|
||||
foregroundServicesRepository,
|
||||
userSwitcherRepository,
|
||||
broadcastDispatcher,
|
||||
bgDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
/** Create a [SecurityRepository] to be used in tests. */
|
||||
fun securityRepository(
|
||||
securityController: SecurityController = FakeSecurityController(),
|
||||
bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(),
|
||||
): SecurityRepository {
|
||||
return SecurityRepositoryImpl(
|
||||
securityController,
|
||||
bgDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
/** Create a [SecurityRepository] to be used in tests. */
|
||||
fun foregroundServicesRepository(
|
||||
fgsManagerController: FakeFgsManagerController = FakeFgsManagerController(),
|
||||
): ForegroundServicesRepository {
|
||||
return ForegroundServicesRepositoryImpl(fgsManagerController)
|
||||
}
|
||||
|
||||
/** Create a [UserSwitcherRepository] to be used in tests. */
|
||||
fun userSwitcherRepository(
|
||||
@Application context: Context = this.context.applicationContext,
|
||||
bgHandler: Handler = Handler(testableLooper.looper),
|
||||
bgDispatcher: CoroutineDispatcher = TestCoroutineDispatcher(),
|
||||
userManager: UserManager = mock(),
|
||||
userTracker: UserTracker = FakeUserTracker(),
|
||||
userSwitcherController: UserSwitcherController = mock(),
|
||||
userInfoController: UserInfoController = FakeUserInfoController(),
|
||||
settings: GlobalSettings = FakeSettings(),
|
||||
): UserSwitcherRepository {
|
||||
return UserSwitcherRepositoryImpl(
|
||||
context,
|
||||
bgHandler,
|
||||
bgDispatcher,
|
||||
userManager,
|
||||
userTracker,
|
||||
userSwitcherController,
|
||||
userInfoController,
|
||||
settings,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.settings
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.pm.UserInfo
|
||||
import android.os.UserHandle
|
||||
import android.test.mock.MockContentResolver
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/** A fake [UserTracker] to be used in tests. */
|
||||
class FakeUserTracker(
|
||||
userId: Int = 0,
|
||||
userHandle: UserHandle = UserHandle.of(userId),
|
||||
userInfo: UserInfo = mock(),
|
||||
userProfiles: List<UserInfo> = emptyList(),
|
||||
userContentResolver: ContentResolver = MockContentResolver(),
|
||||
userContext: Context = mock(),
|
||||
private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
|
||||
) : UserTracker {
|
||||
val callbacks = mutableListOf<UserTracker.Callback>()
|
||||
|
||||
override val userId: Int = userId
|
||||
override val userHandle: UserHandle = userHandle
|
||||
override val userInfo: UserInfo = userInfo
|
||||
override val userProfiles: List<UserInfo> = userProfiles
|
||||
|
||||
override val userContentResolver: ContentResolver = userContentResolver
|
||||
override val userContext: Context = userContext
|
||||
|
||||
override fun addCallback(callback: UserTracker.Callback, executor: Executor) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
|
||||
override fun removeCallback(callback: UserTracker.Callback) {
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
override fun createCurrentUserContext(context: Context): Context {
|
||||
return onCreateCurrentUserContext(context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.statusbar.policy
|
||||
|
||||
import android.app.admin.DeviceAdminInfo
|
||||
import android.content.ComponentName
|
||||
import android.graphics.drawable.Drawable
|
||||
import java.io.PrintWriter
|
||||
|
||||
/** A fake [SecurityController] to be used in tests. */
|
||||
class FakeSecurityController(
|
||||
private val fakeState: FakeState = FakeState(),
|
||||
) : SecurityController {
|
||||
private val callbacks = LinkedHashSet<SecurityController.SecurityControllerCallback>()
|
||||
|
||||
override fun addCallback(callback: SecurityController.SecurityControllerCallback) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
|
||||
override fun removeCallback(callback: SecurityController.SecurityControllerCallback) {
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
/** Update [fakeState], then notify the callbacks. */
|
||||
fun updateState(f: FakeState.() -> Unit) {
|
||||
fakeState.f()
|
||||
callbacks.forEach { it.onStateChanged() }
|
||||
}
|
||||
|
||||
override fun dump(pw: PrintWriter, args: Array<out String>) {}
|
||||
|
||||
override fun isDeviceManaged(): Boolean = fakeState.isDeviceManaged
|
||||
|
||||
override fun hasProfileOwner(): Boolean = fakeState.hasProfileOwner
|
||||
|
||||
override fun hasWorkProfile(): Boolean = fakeState.hasWorkProfile
|
||||
|
||||
override fun isWorkProfileOn(): Boolean = fakeState.isWorkProfileOn
|
||||
|
||||
override fun isProfileOwnerOfOrganizationOwnedDevice(): Boolean =
|
||||
fakeState.isProfileOwnerOfOrganizationOwnedDevice
|
||||
|
||||
override fun getDeviceOwnerName(): String? = fakeState.deviceOwnerName
|
||||
|
||||
override fun getProfileOwnerName(): String? = fakeState.profileOwnerName
|
||||
|
||||
override fun getDeviceOwnerOrganizationName(): String? = fakeState.deviceOwnerOrganizationName
|
||||
|
||||
override fun getWorkProfileOrganizationName(): String? = fakeState.workProfileOrganizationName
|
||||
|
||||
override fun getDeviceOwnerComponentOnAnyUser(): ComponentName? =
|
||||
fakeState.deviceOwnerComponentOnAnyUser
|
||||
|
||||
override fun getDeviceOwnerType(admin: ComponentName?): Int = 0
|
||||
|
||||
override fun isNetworkLoggingEnabled(): Boolean = fakeState.isNetworkLoggingEnabled
|
||||
|
||||
override fun isVpnEnabled(): Boolean = fakeState.isVpnEnabled
|
||||
|
||||
override fun isVpnRestricted(): Boolean = fakeState.isVpnRestricted
|
||||
|
||||
override fun isVpnBranded(): Boolean = fakeState.isVpnBranded
|
||||
|
||||
override fun getPrimaryVpnName(): String? = fakeState.primaryVpnName
|
||||
|
||||
override fun getWorkProfileVpnName(): String? = fakeState.workProfileVpnName
|
||||
|
||||
override fun hasCACertInCurrentUser(): Boolean = fakeState.hasCACertInCurrentUser
|
||||
|
||||
override fun hasCACertInWorkProfile(): Boolean = fakeState.hasCACertInWorkProfile
|
||||
|
||||
override fun onUserSwitched(newUserId: Int) {}
|
||||
|
||||
override fun isParentalControlsEnabled(): Boolean = fakeState.isParentalControlsEnabled
|
||||
|
||||
override fun getDeviceAdminInfo(): DeviceAdminInfo? = fakeState.deviceAdminInfo
|
||||
|
||||
override fun getIcon(info: DeviceAdminInfo?): Drawable? = null
|
||||
|
||||
override fun getLabel(info: DeviceAdminInfo?): CharSequence? = null
|
||||
|
||||
class FakeState(
|
||||
var isDeviceManaged: Boolean = false,
|
||||
var hasProfileOwner: Boolean = false,
|
||||
var hasWorkProfile: Boolean = false,
|
||||
var isWorkProfileOn: Boolean = false,
|
||||
var isProfileOwnerOfOrganizationOwnedDevice: Boolean = false,
|
||||
var deviceOwnerName: String? = null,
|
||||
var profileOwnerName: String? = null,
|
||||
var deviceOwnerOrganizationName: String? = null,
|
||||
var workProfileOrganizationName: String? = null,
|
||||
var deviceOwnerComponentOnAnyUser: ComponentName? = null,
|
||||
var isNetworkLoggingEnabled: Boolean = false,
|
||||
var isVpnEnabled: Boolean = false,
|
||||
var isVpnRestricted: Boolean = false,
|
||||
var isVpnBranded: Boolean = false,
|
||||
var primaryVpnName: String? = null,
|
||||
var workProfileVpnName: String? = null,
|
||||
var hasCACertInCurrentUser: Boolean = false,
|
||||
var hasCACertInWorkProfile: Boolean = false,
|
||||
var isParentalControlsEnabled: Boolean = false,
|
||||
var deviceAdminInfo: DeviceAdminInfo? = null,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.statusbar.policy
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.android.systemui.util.mockito.mock
|
||||
|
||||
/** A fake [UserInfoController] to be used in tests. */
|
||||
class FakeUserInfoController(
|
||||
private val fakeInfo: FakeInfo = FakeInfo(),
|
||||
) : UserInfoController {
|
||||
private val listeners = LinkedHashSet<UserInfoController.OnUserInfoChangedListener>()
|
||||
|
||||
/** Update [fakeInfo], then notify the listeners. */
|
||||
fun updateInfo(f: FakeInfo.() -> Unit) {
|
||||
fakeInfo.f()
|
||||
notifyListeners()
|
||||
}
|
||||
|
||||
private fun notifyListeners() {
|
||||
listeners.forEach { listener ->
|
||||
listener.onUserInfoChanged(fakeInfo.name, fakeInfo.picture, fakeInfo.userAccount)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCallback(listener: UserInfoController.OnUserInfoChangedListener) {
|
||||
listeners.add(listener)
|
||||
|
||||
// The actual implementation notifies the listener when adding it.
|
||||
listener.onUserInfoChanged(fakeInfo.name, fakeInfo.picture, fakeInfo.userAccount)
|
||||
}
|
||||
|
||||
override fun removeCallback(listener: UserInfoController.OnUserInfoChangedListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun reloadUserInfo() {}
|
||||
|
||||
class FakeInfo(
|
||||
var name: String = "",
|
||||
var picture: Drawable = mock(),
|
||||
var userAccount: String = "",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.statusbar.policy
|
||||
|
||||
import com.android.systemui.statusbar.policy.UserSwitcherController.UserSwitchCallback
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
/**
|
||||
* A wrapper around a mocked [UserSwitcherController] to be used in tests.
|
||||
*
|
||||
* Note that this was implemented as a mock wrapper instead of fake implementation of a common
|
||||
* interface given how big the UserSwitcherController grew.
|
||||
*/
|
||||
class MockUserSwitcherControllerWrapper(
|
||||
currentUserName: String = "",
|
||||
) {
|
||||
val controller: UserSwitcherController = mock()
|
||||
private val callbacks = LinkedHashSet<UserSwitchCallback>()
|
||||
|
||||
var currentUserName = currentUserName
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
field = value
|
||||
notifyCallbacks()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyCallbacks() {
|
||||
callbacks.forEach { it.onUserSwitched() }
|
||||
}
|
||||
|
||||
init {
|
||||
whenever(controller.addUserSwitchCallback(any())).then { invocation ->
|
||||
callbacks.add(invocation.arguments.first() as UserSwitchCallback)
|
||||
}
|
||||
|
||||
whenever(controller.removeUserSwitchCallback(any())).then { invocation ->
|
||||
callbacks.remove(invocation.arguments.first() as UserSwitchCallback)
|
||||
}
|
||||
|
||||
whenever(controller.currentUserName).thenAnswer { this.currentUserName }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.truth.correspondence
|
||||
|
||||
import com.android.internal.logging.testing.UiEventLoggerFake
|
||||
import com.android.internal.logging.testing.UiEventLoggerFake.FakeUiEvent
|
||||
import com.google.common.truth.Correspondence
|
||||
|
||||
/** Instances of [Correspondence] to match a [UiEventLoggerFake.FakeUiEvent] with Truth. */
|
||||
object FakeUiEvent {
|
||||
val EVENT_ID =
|
||||
Correspondence.transforming<FakeUiEvent, Int>(
|
||||
{ it?.eventId },
|
||||
"has a eventId of",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.truth.correspondence
|
||||
|
||||
import android.metrics.LogMaker
|
||||
import com.google.common.truth.Correspondence
|
||||
|
||||
/** Instances of [Correspondence] to match a [LogMaker] with Truth. */
|
||||
object LogMaker {
|
||||
val CATEGORY =
|
||||
Correspondence.transforming<LogMaker, Int>(
|
||||
{ it?.category },
|
||||
"has a category of",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user