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";
|
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.
|
* (boolean) Whether the clipboard overlay is enabled.
|
||||||
|
|||||||
@@ -7,3 +7,11 @@ packages/SystemUI/src/com/android/systemui/keyguard/dagger
|
|||||||
packages/SystemUI/src/com/android/systemui/keyguard/domain
|
packages/SystemUI/src/com/android/systemui/keyguard/domain
|
||||||
packages/SystemUI/src/com/android/systemui/keyguard/shared
|
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: [
|
libs: [
|
||||||
"android.test.runner",
|
"android.test.runner",
|
||||||
"android.test.base",
|
"android.test.base",
|
||||||
|
"android.test.mock",
|
||||||
],
|
],
|
||||||
kotlincflags: ["-Xjvm-default=enable"],
|
kotlincflags: ["-Xjvm-default=enable"],
|
||||||
aaptflags: [
|
aaptflags: [
|
||||||
|
|||||||
@@ -311,8 +311,7 @@ class ActivityLaunchAnimator(
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fromView(view: View, cujType: Int? = null): Controller? {
|
fun fromView(view: View, cujType: Int? = null): Controller? {
|
||||||
if (view.parent !is ViewGroup) {
|
if (view.parent !is ViewGroup) {
|
||||||
// TODO(b/192194319): Throw instead of just logging.
|
Log.e(
|
||||||
Log.wtf(
|
|
||||||
TAG,
|
TAG,
|
||||||
"Skipping animation as view $view is not attached to a ViewGroup",
|
"Skipping animation as view $view is not attached to a ViewGroup",
|
||||||
Exception()
|
Exception()
|
||||||
|
|||||||
@@ -113,6 +113,19 @@ constructor(
|
|||||||
}
|
}
|
||||||
val animateFrom = animatedParent?.dialogContentWithBackground ?: view
|
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.
|
// 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) {
|
if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
|
||||||
Log.e(TAG, "Not running dialog launch animation as there is already one running")
|
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
|
openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
|
||||||
?: throw IllegalStateException(
|
?: throw IllegalStateException(
|
||||||
"The animateFrom dialog was not animated using " +
|
"The animateFrom dialog was not animated using " +
|
||||||
"DialogLaunchAnimator.showFrom(View|Dialog)")
|
"DialogLaunchAnimator.showFrom(View|Dialog)"
|
||||||
|
)
|
||||||
showFromView(
|
showFromView(
|
||||||
dialog, view, animateBackgroundBoundsChange = animateBackgroundBoundsChange, cuj = cuj)
|
dialog,
|
||||||
|
view,
|
||||||
|
animateBackgroundBoundsChange = animateBackgroundBoundsChange,
|
||||||
|
cuj = cuj
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -197,7 +215,7 @@ constructor(
|
|||||||
// bouncer.
|
// bouncer.
|
||||||
if (
|
if (
|
||||||
!dialog.isShowing ||
|
!dialog.isShowing ||
|
||||||
(!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
|
(!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -556,11 +574,12 @@ private class AnimatedDialog(
|
|||||||
window.setDecorFitsSystemWindows(false)
|
window.setDecorFitsSystemWindows(false)
|
||||||
val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
|
val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
|
||||||
viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
|
viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
|
||||||
val type = if (wasFittingNavigationBars) {
|
val type =
|
||||||
WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars()
|
if (wasFittingNavigationBars) {
|
||||||
} else {
|
WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars()
|
||||||
WindowInsets.Type.displayCutout()
|
} else {
|
||||||
}
|
WindowInsets.Type.displayCutout()
|
||||||
|
}
|
||||||
|
|
||||||
val insets = windowInsets.getInsets(type)
|
val insets = windowInsets.getInsets(type)
|
||||||
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
|
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
|
package com.android.systemui.animation
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
/** A view that can expand/launch into an app or a dialog. */
|
/** A view that can expand/launch into an app or a dialog. */
|
||||||
interface LaunchableView {
|
interface LaunchableView {
|
||||||
/**
|
/**
|
||||||
* Set whether this view should block/prevent all visibility changes. This ensures that this
|
* 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
|
* view:
|
||||||
|
* - remains invisible during the launch animation given that it is ghosted and already drawn
|
||||||
* somewhere else.
|
* 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
|
* 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.
|
* [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)
|
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",
|
"testables",
|
||||||
"truth-prebuilt",
|
"truth-prebuilt",
|
||||||
"androidx.test.uiautomator",
|
"androidx.test.uiautomator",
|
||||||
|
"kotlinx_coroutines_test",
|
||||||
|
],
|
||||||
|
|
||||||
|
libs: [
|
||||||
|
"android.test.mock",
|
||||||
],
|
],
|
||||||
|
|
||||||
kotlincflags: ["-Xjvm-default=all"],
|
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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
<!-- TODO(b/242040009): Remove this file. -->
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -16,16 +16,17 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc -->
|
<!-- 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
|
<com.android.systemui.qs.FooterActionsView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/footer_actions_height"
|
android:layout_height="@dimen/footer_actions_height"
|
||||||
android:elevation="@dimen/qs_panel_elevation"
|
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:paddingBottom="@dimen/qs_footer_actions_bottom_padding"
|
||||||
android:background="@drawable/qs_footer_actions_background"
|
android:background="@drawable/qs_footer_actions_background"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical|end"
|
||||||
android:layout_gravity="bottom"
|
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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
<!-- TODO(b/242040009): Remove this file. -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -380,6 +380,7 @@
|
|||||||
|
|
||||||
<!-- (48dp - 40dp) / 2 -->
|
<!-- (48dp - 40dp) / 2 -->
|
||||||
<dimen name="qs_footer_action_inset">4dp</dimen>
|
<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_actions_bottom_padding">4dp</dimen>
|
||||||
<dimen name="qs_footer_action_inset_negative">-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.internal.annotations.VisibleForTesting
|
||||||
import com.android.systemui.Dumpable
|
import com.android.systemui.Dumpable
|
||||||
import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger
|
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.SysUISingleton
|
||||||
import com.android.systemui.dagger.qualifiers.Background
|
import com.android.systemui.dagger.qualifiers.Background
|
||||||
import com.android.systemui.dump.DumpManager
|
import com.android.systemui.dump.DumpManager
|
||||||
@@ -38,6 +40,8 @@ import com.android.systemui.settings.UserTracker
|
|||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
data class ReceiverData(
|
data class ReceiverData(
|
||||||
val receiver: BroadcastReceiver,
|
val receiver: BroadcastReceiver,
|
||||||
@@ -153,6 +157,55 @@ open class BroadcastDispatcher @Inject constructor(
|
|||||||
.sendToTarget()
|
.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) {
|
private fun checkFilter(filter: IntentFilter) {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
if (filter.countActions() == 0) sb.append("Filter must contain at least one action. ")
|
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.people.PeopleModule;
|
||||||
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
|
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
|
||||||
import com.android.systemui.privacy.PrivacyModule;
|
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.recents.Recents;
|
||||||
import com.android.systemui.screenshot.dagger.ScreenshotModule;
|
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.settings.dagger.MultiUserUtilsModule;
|
||||||
import com.android.systemui.shade.ShadeController;
|
import com.android.systemui.shade.ShadeController;
|
||||||
import com.android.systemui.smartspace.dagger.SmartspaceModule;
|
import com.android.systemui.smartspace.dagger.SmartspaceModule;
|
||||||
@@ -122,6 +126,7 @@ import dagger.Provides;
|
|||||||
DemoModeModule.class,
|
DemoModeModule.class,
|
||||||
FalsingModule.class,
|
FalsingModule.class,
|
||||||
FlagsModule.class,
|
FlagsModule.class,
|
||||||
|
FooterActionsModule.class,
|
||||||
LogModule.class,
|
LogModule.class,
|
||||||
MediaProjectionModule.class,
|
MediaProjectionModule.class,
|
||||||
PeopleHubModule.class,
|
PeopleHubModule.class,
|
||||||
@@ -132,6 +137,7 @@ import dagger.Provides;
|
|||||||
ScreenshotModule.class,
|
ScreenshotModule.class,
|
||||||
SensorModule.class,
|
SensorModule.class,
|
||||||
MultiUserUtilsModule.class,
|
MultiUserUtilsModule.class,
|
||||||
|
SecurityRepositoryModule.class,
|
||||||
SettingsUtilModule.class,
|
SettingsUtilModule.class,
|
||||||
SmartRepliesInflationModule.class,
|
SmartRepliesInflationModule.class,
|
||||||
SmartspaceModule.class,
|
SmartspaceModule.class,
|
||||||
@@ -258,4 +264,7 @@ public abstract class SystemUIModule {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract FgsManagerController bindFgsManagerController(FgsManagerControllerImpl impl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ public class Flags {
|
|||||||
public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
|
public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
|
||||||
new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
|
new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
|
||||||
|
|
||||||
|
public static final UnreleasedFlag NEW_FOOTER_ACTIONS = new UnreleasedFlag(507, true);
|
||||||
|
|
||||||
/***************************************/
|
/***************************************/
|
||||||
// 600- status bar
|
// 600- status bar
|
||||||
public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =
|
public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =
|
||||||
|
|||||||
@@ -40,11 +40,13 @@ import android.widget.Button
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.GuardedBy
|
import androidx.annotation.GuardedBy
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED
|
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_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.internal.jank.InteractionJankMonitor
|
||||||
import com.android.systemui.Dumpable
|
import com.android.systemui.Dumpable
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
@@ -66,9 +68,73 @@ import java.util.Objects
|
|||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.max
|
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
|
@SysUISingleton
|
||||||
class FgsManagerController @Inject constructor(
|
class FgsManagerControllerImpl @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@Main private val mainExecutor: Executor,
|
@Main private val mainExecutor: Executor,
|
||||||
@Background private val backgroundExecutor: Executor,
|
@Background private val backgroundExecutor: Executor,
|
||||||
@@ -80,22 +146,32 @@ class FgsManagerController @Inject constructor(
|
|||||||
private val dialogLaunchAnimator: DialogLaunchAnimator,
|
private val dialogLaunchAnimator: DialogLaunchAnimator,
|
||||||
private val broadcastDispatcher: BroadcastDispatcher,
|
private val broadcastDispatcher: BroadcastDispatcher,
|
||||||
private val dumpManager: DumpManager
|
private val dumpManager: DumpManager
|
||||||
) : IForegroundServiceObserver.Stub(), Dumpable {
|
) : IForegroundServiceObserver.Stub(), Dumpable, FgsManagerController {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val INTERACTION_JANK_TAG = "active_background_apps"
|
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_ENABLED = true
|
||||||
private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false
|
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
|
private set
|
||||||
|
|
||||||
var isAvailable = false
|
val _isAvailable = MutableStateFlow(false)
|
||||||
private set
|
override val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow()
|
||||||
var showFooterDot = false
|
|
||||||
private set
|
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()
|
private val lock = Any()
|
||||||
|
|
||||||
@@ -133,15 +209,7 @@ class FgsManagerController @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnNumberOfPackagesChangedListener {
|
override fun init() {
|
||||||
fun onNumberOfPackagesChanged(numPackages: Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnDialogDismissedListener {
|
|
||||||
fun onDialogDismissed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun init() {
|
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return
|
return
|
||||||
@@ -160,19 +228,26 @@ class FgsManagerController @Inject constructor(
|
|||||||
NAMESPACE_SYSTEMUI,
|
NAMESPACE_SYSTEMUI,
|
||||||
backgroundExecutor
|
backgroundExecutor
|
||||||
) {
|
) {
|
||||||
isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable)
|
_isAvailable.value = it.getBoolean(TASK_MANAGER_ENABLED, _isAvailable.value)
|
||||||
showFooterDot =
|
_showFooterDot.value =
|
||||||
it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, showFooterDot)
|
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,
|
NAMESPACE_SYSTEMUI,
|
||||||
TASK_MANAGER_ENABLED, DEFAULT_TASK_MANAGER_ENABLED
|
TASK_MANAGER_ENABLED, DEFAULT_TASK_MANAGER_ENABLED
|
||||||
)
|
)
|
||||||
showFooterDot = deviceConfigProxy.getBoolean(
|
_showFooterDot.value = deviceConfigProxy.getBoolean(
|
||||||
NAMESPACE_SYSTEMUI,
|
NAMESPACE_SYSTEMUI,
|
||||||
TASK_MANAGER_SHOW_FOOTER_DOT, DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT
|
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)
|
dumpManager.registerDumpable(this)
|
||||||
|
|
||||||
@@ -220,42 +295,45 @@ class FgsManagerController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GuardedBy("lock")
|
@GuardedBy("lock")
|
||||||
val onNumberOfPackagesChangedListeners: MutableSet<OnNumberOfPackagesChangedListener> =
|
private val onNumberOfPackagesChangedListeners =
|
||||||
mutableSetOf()
|
mutableSetOf<FgsManagerController.OnNumberOfPackagesChangedListener>()
|
||||||
|
|
||||||
@GuardedBy("lock")
|
@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) {
|
synchronized(lock) {
|
||||||
onNumberOfPackagesChangedListeners.add(listener)
|
onNumberOfPackagesChangedListeners.add(listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
|
override fun removeOnNumberOfPackagesChangedListener(
|
||||||
|
listener: FgsManagerController.OnNumberOfPackagesChangedListener
|
||||||
|
) {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
onNumberOfPackagesChangedListeners.remove(listener)
|
onNumberOfPackagesChangedListeners.remove(listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addOnDialogDismissedListener(listener: OnDialogDismissedListener) {
|
override fun addOnDialogDismissedListener(
|
||||||
|
listener: FgsManagerController.OnDialogDismissedListener
|
||||||
|
) {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
onDialogDismissedListeners.add(listener)
|
onDialogDismissedListeners.add(listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener) {
|
override fun removeOnDialogDismissedListener(
|
||||||
|
listener: FgsManagerController.OnDialogDismissedListener
|
||||||
|
) {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
onDialogDismissedListeners.remove(listener)
|
onDialogDismissedListeners.remove(listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNumRunningPackages(): Int {
|
|
||||||
synchronized(lock) {
|
|
||||||
return getNumVisiblePackagesLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNumVisiblePackagesLocked(): Int {
|
private fun getNumVisiblePackagesLocked(): Int {
|
||||||
return runningServiceTokens.keys.count {
|
return runningServiceTokens.keys.count {
|
||||||
it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId)
|
it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId)
|
||||||
@@ -266,7 +344,7 @@ class FgsManagerController @Inject constructor(
|
|||||||
val num = getNumVisiblePackagesLocked()
|
val num = getNumVisiblePackagesLocked()
|
||||||
if (num != lastNumberOfVisiblePackages) {
|
if (num != lastNumberOfVisiblePackages) {
|
||||||
lastNumberOfVisiblePackages = num
|
lastNumberOfVisiblePackages = num
|
||||||
changesSinceDialog = true
|
newChangesSinceDialogWasDismissed = true
|
||||||
onNumberOfPackagesChangedListeners.forEach {
|
onNumberOfPackagesChangedListeners.forEach {
|
||||||
backgroundExecutor.execute {
|
backgroundExecutor.execute {
|
||||||
it.onNumberOfPackagesChanged(num)
|
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) {
|
synchronized(lock) {
|
||||||
if (dialog == null) {
|
if (dialog == null) {
|
||||||
|
|
||||||
@@ -302,7 +392,7 @@ class FgsManagerController @Inject constructor(
|
|||||||
this.dialog = dialog
|
this.dialog = dialog
|
||||||
|
|
||||||
dialog.setOnDismissListener {
|
dialog.setOnDismissListener {
|
||||||
changesSinceDialog = false
|
newChangesSinceDialogWasDismissed = false
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
this.dialog = null
|
this.dialog = null
|
||||||
updateAppItemsLocked()
|
updateAppItemsLocked()
|
||||||
@@ -505,6 +595,13 @@ class FgsManagerController @Inject constructor(
|
|||||||
PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI,
|
PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI,
|
||||||
PowerExemptionManager.REASON_ROLE_DIALER,
|
PowerExemptionManager.REASON_ROLE_DIALER,
|
||||||
PowerExemptionManager.REASON_SYSTEM_MODULE -> UIControl.HIDE_BUTTON
|
PowerExemptionManager.REASON_SYSTEM_MODULE -> UIControl.HIDE_BUTTON
|
||||||
|
|
||||||
|
PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE ->
|
||||||
|
if (showStopBtnForUserAllowlistedApps) {
|
||||||
|
UIControl.NORMAL
|
||||||
|
} else {
|
||||||
|
UIControl.HIDE_BUTTON
|
||||||
|
}
|
||||||
else -> UIControl.NORMAL
|
else -> UIControl.NORMAL
|
||||||
}
|
}
|
||||||
uiControlInitialized = true
|
uiControlInitialized = true
|
||||||
@@ -623,7 +720,7 @@ class FgsManagerController @Inject constructor(
|
|||||||
val pw = IndentingPrintWriter(printwriter)
|
val pw = IndentingPrintWriter(printwriter)
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
pw.println("current user profiles = $currentProfileIds")
|
pw.println("current user profiles = $currentProfileIds")
|
||||||
pw.println("changesSinceDialog=$changesSinceDialog")
|
pw.println("newChangesSinceDialogWasShown=$newChangesSinceDialogWasDismissed")
|
||||||
pw.println("Running service tokens: [")
|
pw.println("Running service tokens: [")
|
||||||
pw.indentIfPossible {
|
pw.indentIfPossible {
|
||||||
runningServiceTokens.forEach { (userPackage, startTimeAndTokens) ->
|
runningServiceTokens.forEach { (userPackage, startTimeAndTokens) ->
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import javax.inject.Provider
|
|||||||
* determined by [buttonsVisibleState]
|
* determined by [buttonsVisibleState]
|
||||||
*/
|
*/
|
||||||
@QSScope
|
@QSScope
|
||||||
|
// TODO(b/242040009): Remove this file.
|
||||||
internal class FooterActionsController @Inject constructor(
|
internal class FooterActionsController @Inject constructor(
|
||||||
view: FooterActionsView,
|
view: FooterActionsView,
|
||||||
multiUserSwitchControllerFactory: MultiUserSwitchController.Factory,
|
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,
|
* 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
|
* 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) {
|
class FooterActionsView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) {
|
||||||
private lateinit var settingsContainer: View
|
private lateinit var settingsContainer: View
|
||||||
private lateinit var multiUserSwitch: MultiUserSwitch
|
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
|
* Footer entry point for the foreground service manager
|
||||||
*/
|
*/
|
||||||
|
// TODO(b/242040009): Remove this file.
|
||||||
@QSScope
|
@QSScope
|
||||||
public class QSFgsManagerFooter implements View.OnClickListener,
|
public class QSFgsManagerFooter implements View.OnClickListener,
|
||||||
FgsManagerController.OnDialogDismissedListener,
|
FgsManagerController.OnDialogDismissedListener,
|
||||||
@@ -149,9 +150,11 @@ public class QSFgsManagerFooter implements View.OnClickListener,
|
|||||||
mNumberView.setContentDescription(text);
|
mNumberView.setContentDescription(text);
|
||||||
if (mFgsManagerController.shouldUpdateFooterVisibility()) {
|
if (mFgsManagerController.shouldUpdateFooterVisibility()) {
|
||||||
mRootView.setVisibility(mNumPackages > 0
|
mRootView.setVisibility(mNumPackages > 0
|
||||||
&& mFgsManagerController.isAvailable() ? View.VISIBLE : View.GONE);
|
&& mFgsManagerController.isAvailable().getValue() ? View.VISIBLE
|
||||||
int dotVis = mFgsManagerController.getShowFooterDot()
|
: View.GONE);
|
||||||
&& mFgsManagerController.getChangesSinceDialog() ? View.VISIBLE : View.GONE;
|
int dotVis = mFgsManagerController.getShowFooterDot().getValue()
|
||||||
|
&& mFgsManagerController.getNewChangesSinceDialogWasDismissed()
|
||||||
|
? View.VISIBLE : View.GONE;
|
||||||
mDotView.setVisibility(dotVis);
|
mDotView.setVisibility(dotVis);
|
||||||
mCollapsedDotView.setVisibility(dotVis);
|
mCollapsedDotView.setVisibility(dotVis);
|
||||||
if (mVisibilityChangedListener != null) {
|
if (mVisibilityChangedListener != null) {
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ import android.view.ViewTreeObserver;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.LifecycleRegistry;
|
||||||
|
|
||||||
import com.android.keyguard.BouncerPanelExpansionCalculator;
|
import com.android.keyguard.BouncerPanelExpansionCalculator;
|
||||||
import com.android.systemui.Dumpable;
|
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.Interpolators;
|
||||||
import com.android.systemui.animation.ShadeInterpolation;
|
import com.android.systemui.animation.ShadeInterpolation;
|
||||||
import com.android.systemui.dump.DumpManager;
|
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.media.MediaHost;
|
||||||
import com.android.systemui.plugins.FalsingManager;
|
import com.android.systemui.plugins.FalsingManager;
|
||||||
import com.android.systemui.plugins.qs.QS;
|
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.plugins.statusbar.StatusBarStateController;
|
||||||
import com.android.systemui.qs.customize.QSCustomizerController;
|
import com.android.systemui.qs.customize.QSCustomizerController;
|
||||||
import com.android.systemui.qs.dagger.QSFragmentComponent;
|
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.CommandQueue;
|
||||||
import com.android.systemui.statusbar.StatusBarState;
|
import com.android.systemui.statusbar.StatusBarState;
|
||||||
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
|
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 QSFragmentComponent.Factory mQsComponentFactory;
|
||||||
private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger;
|
private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger;
|
||||||
private final QSTileHost mHost;
|
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 mShowCollapsedOnKeyguard;
|
||||||
private boolean mLastKeyguardAndExpanded;
|
private boolean mLastKeyguardAndExpanded;
|
||||||
/**
|
/**
|
||||||
@@ -119,8 +130,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
private QSPanelController mQSPanelController;
|
private QSPanelController mQSPanelController;
|
||||||
private QuickQSPanelController mQuickQSPanelController;
|
private QuickQSPanelController mQuickQSPanelController;
|
||||||
private QSCustomizerController mQSCustomizerController;
|
private QSCustomizerController mQSCustomizerController;
|
||||||
|
@Nullable
|
||||||
private FooterActionsController mQSFooterActionController;
|
private FooterActionsController mQSFooterActionController;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
private FooterActionsViewModel mQSFooterActionsViewModel;
|
||||||
|
@Nullable
|
||||||
private ScrollListener mScrollListener;
|
private ScrollListener mScrollListener;
|
||||||
/**
|
/**
|
||||||
* When true, QS will translate from outside the screen. It will be clipped with parallax
|
* 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,
|
KeyguardBypassController keyguardBypassController,
|
||||||
QSFragmentComponent.Factory qsComponentFactory,
|
QSFragmentComponent.Factory qsComponentFactory,
|
||||||
QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger,
|
QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger,
|
||||||
FalsingManager falsingManager, DumpManager dumpManager) {
|
FalsingManager falsingManager, DumpManager dumpManager, FeatureFlags featureFlags,
|
||||||
|
NewFooterActionsController newFooterActionsController,
|
||||||
|
FooterActionsViewModel.Factory footerActionsViewModelFactory) {
|
||||||
mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
|
mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
|
||||||
mQsMediaHost = qsMediaHost;
|
mQsMediaHost = qsMediaHost;
|
||||||
mQqsMediaHost = qqsMediaHost;
|
mQqsMediaHost = qqsMediaHost;
|
||||||
@@ -173,6 +189,10 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
mBypassController = keyguardBypassController;
|
mBypassController = keyguardBypassController;
|
||||||
mStatusBarStateController = statusBarStateController;
|
mStatusBarStateController = statusBarStateController;
|
||||||
mDumpManager = dumpManager;
|
mDumpManager = dumpManager;
|
||||||
|
mFeatureFlags = featureFlags;
|
||||||
|
mNewFooterActionsController = newFooterActionsController;
|
||||||
|
mFooterActionsViewModelFactory = footerActionsViewModelFactory;
|
||||||
|
mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -193,11 +213,22 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this);
|
QSFragmentComponent qsFragmentComponent = mQsComponentFactory.create(this);
|
||||||
mQSPanelController = qsFragmentComponent.getQSPanelController();
|
mQSPanelController = qsFragmentComponent.getQSPanelController();
|
||||||
mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController();
|
mQuickQSPanelController = qsFragmentComponent.getQuickQSPanelController();
|
||||||
mQSFooterActionController = qsFragmentComponent.getQSFooterActionController();
|
|
||||||
|
|
||||||
mQSPanelController.init();
|
mQSPanelController.init();
|
||||||
mQuickQSPanelController.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 = view.findViewById(R.id.expanded_qs_scroll_view);
|
||||||
mQSPanelScrollView.addOnLayoutChangeListener(
|
mQSPanelScrollView.addOnLayoutChangeListener(
|
||||||
@@ -283,6 +314,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
mDumpManager.unregisterDumpable(mContainer.getClass().getName());
|
mDumpManager.unregisterDumpable(mContainer.getClass().getName());
|
||||||
}
|
}
|
||||||
mDumpManager.unregisterDumpable(getClass().getName());
|
mDumpManager.unregisterDumpable(getClass().getName());
|
||||||
|
mListeningAndVisibilityLifecycleOwner.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -395,7 +427,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
mContainer.disable(state1, state2, animate);
|
mContainer.disable(state1, state2, animate);
|
||||||
mHeader.disable(state1, state2, animate);
|
mHeader.disable(state1, state2, animate);
|
||||||
mFooter.disable(state1, state2, animate);
|
mFooter.disable(state1, state2, animate);
|
||||||
mQSFooterActionController.disable(state2);
|
if (mQSFooterActionController != null) {
|
||||||
|
mQSFooterActionController.disable(state2);
|
||||||
|
}
|
||||||
updateQsState();
|
updateQsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +449,11 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
boolean footerVisible = qsPanelVisible && (expanded || !keyguardShowing || mHeaderAnimating
|
boolean footerVisible = qsPanelVisible && (expanded || !keyguardShowing || mHeaderAnimating
|
||||||
|| mShowCollapsedOnKeyguard);
|
|| mShowCollapsedOnKeyguard);
|
||||||
mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
|
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)
|
mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
|
||||||
|| (expanded && !mStackScrollerOverscrolling));
|
|| (expanded && !mStackScrollerOverscrolling));
|
||||||
mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
|
mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
|
||||||
@@ -482,7 +520,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
}
|
}
|
||||||
|
|
||||||
mFooter.setKeyguardShowing(keyguardShowing);
|
mFooter.setKeyguardShowing(keyguardShowing);
|
||||||
mQSFooterActionController.setKeyguardShowing(keyguardShowing);
|
if (mQSFooterActionController != null) {
|
||||||
|
mQSFooterActionController.setKeyguardShowing(keyguardShowing);
|
||||||
|
}
|
||||||
updateQsState();
|
updateQsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +538,10 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
if (DEBUG) Log.d(TAG, "setListening " + listening);
|
if (DEBUG) Log.d(TAG, "setListening " + listening);
|
||||||
mListening = listening;
|
mListening = listening;
|
||||||
mQSContainerImplController.setListening(listening && mQsVisible);
|
mQSContainerImplController.setListening(listening && mQsVisible);
|
||||||
mQSFooterActionController.setListening(listening && mQsVisible);
|
if (mQSFooterActionController != null) {
|
||||||
|
mQSFooterActionController.setListening(listening && mQsVisible);
|
||||||
|
}
|
||||||
|
mListeningAndVisibilityLifecycleOwner.updateState();
|
||||||
updateQsPanelControllerListening();
|
updateQsPanelControllerListening();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,6 +554,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
if (DEBUG) Log.d(TAG, "setQsVisible " + visible);
|
if (DEBUG) Log.d(TAG, "setQsVisible " + visible);
|
||||||
mQsVisible = visible;
|
mQsVisible = visible;
|
||||||
setListening(mListening);
|
setListening(mListening);
|
||||||
|
mListeningAndVisibilityLifecycleOwner.updateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -602,7 +646,12 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
|
mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
|
||||||
float footerActionsExpansion =
|
float footerActionsExpansion =
|
||||||
onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
|
onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
|
||||||
mQSFooterActionController.setExpansion(footerActionsExpansion);
|
if (mQSFooterActionController != null) {
|
||||||
|
mQSFooterActionController.setExpansion(footerActionsExpansion);
|
||||||
|
} else {
|
||||||
|
mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
|
||||||
|
mInSplitShade);
|
||||||
|
}
|
||||||
mQSPanelController.setRevealExpansion(expansion);
|
mQSPanelController.setRevealExpansion(expansion);
|
||||||
mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
|
mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
|
||||||
mQuickQSPanelController.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();
|
boolean customizing = isCustomizing();
|
||||||
mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
|
mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
|
||||||
mFooter.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);
|
mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
|
||||||
// Let the panel know the position changed and it needs to update where notifications
|
// Let the panel know the position changed and it needs to update where notifications
|
||||||
// and whatnot are.
|
// and whatnot are.
|
||||||
@@ -860,4 +913,56 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
|||||||
}
|
}
|
||||||
return "GONE";
|
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;
|
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 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.DevicePolicyEventLogger;
|
||||||
import android.app.admin.DevicePolicyManager;
|
import android.app.admin.DevicePolicyManager;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.pm.UserInfo;
|
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.os.UserHandle;
|
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.util.Log;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.Window;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
|
|
||||||
import com.android.internal.jank.InteractionJankMonitor;
|
|
||||||
import com.android.internal.util.FrameworkStatsLog;
|
import com.android.internal.util.FrameworkStatsLog;
|
||||||
import com.android.systemui.FontSizeUtils;
|
import com.android.systemui.FontSizeUtils;
|
||||||
import com.android.systemui.R;
|
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.broadcast.BroadcastDispatcher;
|
||||||
|
import com.android.systemui.common.shared.model.Icon;
|
||||||
import com.android.systemui.dagger.qualifiers.Background;
|
import com.android.systemui.dagger.qualifiers.Background;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
import com.android.systemui.plugins.ActivityStarter;
|
|
||||||
import com.android.systemui.qs.dagger.QSScope;
|
import com.android.systemui.qs.dagger.QSScope;
|
||||||
import com.android.systemui.settings.UserTracker;
|
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig;
|
||||||
import com.android.systemui.statusbar.phone.SystemUIDialog;
|
import com.android.systemui.security.data.model.SecurityModel;
|
||||||
import com.android.systemui.statusbar.policy.SecurityController;
|
import com.android.systemui.statusbar.policy.SecurityController;
|
||||||
import com.android.systemui.util.ViewController;
|
import com.android.systemui.util.ViewController;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
/** ViewController for the footer actions. */
|
||||||
|
// TODO(b/242040009): Remove this class.
|
||||||
@QSScope
|
@QSScope
|
||||||
class QSSecurityFooter extends ViewController<View>
|
public class QSSecurityFooter extends ViewController<View>
|
||||||
implements OnClickListener, DialogInterface.OnClickListener,
|
implements OnClickListener, VisibilityChangedDispatcher {
|
||||||
VisibilityChangedDispatcher {
|
|
||||||
protected static final String TAG = "QSSecurityFooter";
|
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 TextView mFooterText;
|
||||||
private final ImageView mPrimaryFooterIcon;
|
private final ImageView mPrimaryFooterIcon;
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private final DevicePolicyManager mDpm;
|
|
||||||
private final Callback mCallback = new Callback();
|
private final Callback mCallback = new Callback();
|
||||||
private final SecurityController mSecurityController;
|
private final SecurityController mSecurityController;
|
||||||
private final ActivityStarter mActivityStarter;
|
|
||||||
private final Handler mMainHandler;
|
private final Handler mMainHandler;
|
||||||
private final UserTracker mUserTracker;
|
|
||||||
private final DialogLaunchAnimator mDialogLaunchAnimator;
|
|
||||||
private final BroadcastDispatcher mBroadcastDispatcher;
|
private final BroadcastDispatcher mBroadcastDispatcher;
|
||||||
|
private final QSSecurityFooterUtils mQSSecurityFooterUtils;
|
||||||
|
|
||||||
private final AtomicBoolean mShouldUseSettingsButton = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
private AlertDialog mDialog;
|
|
||||||
protected H mHandler;
|
protected H mHandler;
|
||||||
|
|
||||||
private boolean mIsVisible;
|
private boolean mIsVisible;
|
||||||
|
private boolean mIsClickable;
|
||||||
@Nullable
|
@Nullable
|
||||||
private CharSequence mFooterTextContent = null;
|
private CharSequence mFooterTextContent = null;
|
||||||
private int mFooterIconId;
|
private Icon mFooterIcon;
|
||||||
@Nullable
|
|
||||||
private Drawable mPrimaryFooterIconDrawable;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private VisibilityChangedDispatcher.OnVisibilityChangedListener mVisibilityChangedListener;
|
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
|
@Inject
|
||||||
QSSecurityFooter(@Named(QS_SECURITY_FOOTER_VIEW) View rootView,
|
QSSecurityFooter(@Named(QS_SECURITY_FOOTER_VIEW) View rootView,
|
||||||
UserTracker userTracker, @Main Handler mainHandler,
|
@Main Handler mainHandler, SecurityController securityController,
|
||||||
ActivityStarter activityStarter, SecurityController securityController,
|
@Background Looper bgLooper, BroadcastDispatcher broadcastDispatcher,
|
||||||
DialogLaunchAnimator dialogLaunchAnimator, @Background Looper bgLooper,
|
QSSecurityFooterUtils qSSecurityFooterUtils) {
|
||||||
BroadcastDispatcher broadcastDispatcher) {
|
|
||||||
super(rootView);
|
super(rootView);
|
||||||
mFooterText = mView.findViewById(R.id.footer_text);
|
mFooterText = mView.findViewById(R.id.footer_text);
|
||||||
mPrimaryFooterIcon = mView.findViewById(R.id.primary_footer_icon);
|
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();
|
mContext = rootView.getContext();
|
||||||
mDpm = rootView.getContext().getSystemService(DevicePolicyManager.class);
|
|
||||||
mMainHandler = mainHandler;
|
|
||||||
mActivityStarter = activityStarter;
|
|
||||||
mSecurityController = securityController;
|
mSecurityController = securityController;
|
||||||
|
mMainHandler = mainHandler;
|
||||||
mHandler = new H(bgLooper);
|
mHandler = new H(bgLooper);
|
||||||
mUserTracker = userTracker;
|
|
||||||
mDialogLaunchAnimator = dialogLaunchAnimator;
|
|
||||||
mBroadcastDispatcher = broadcastDispatcher;
|
mBroadcastDispatcher = broadcastDispatcher;
|
||||||
|
mQSSecurityFooterUtils = qSSecurityFooterUtils;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -287,8 +166,9 @@ class QSSecurityFooter extends ViewController<View>
|
|||||||
.write();
|
.write();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(b/242040009): Remove this.
|
||||||
public void showDeviceMonitoringDialog() {
|
public void showDeviceMonitoringDialog() {
|
||||||
createDialog();
|
mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshState() {
|
public void refreshState() {
|
||||||
@@ -296,590 +176,30 @@ class QSSecurityFooter extends ViewController<View>
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleRefreshState() {
|
private void handleRefreshState() {
|
||||||
final boolean isDeviceManaged = mSecurityController.isDeviceManaged();
|
SecurityModel securityModel = SecurityModel.create(mSecurityController);
|
||||||
final UserInfo currentUser = mUserTracker.getUserInfo();
|
SecurityButtonConfig buttonConfig = mQSSecurityFooterUtils.getButtonConfig(securityModel);
|
||||||
final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null
|
|
||||||
&& currentUser.isDemo();
|
if (buttonConfig == null) {
|
||||||
final boolean hasWorkProfile = mSecurityController.hasWorkProfile();
|
mIsVisible = false;
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
mView.setClickable(true);
|
mIsVisible = true;
|
||||||
mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE);
|
mIsClickable = buttonConfig.isClickable();
|
||||||
}
|
mFooterTextContent = buttonConfig.getText();
|
||||||
// Update the string
|
mFooterIcon = buttonConfig.getIcon();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the primary icon
|
// Update the UI.
|
||||||
if (isParentalControlsEnabled) {
|
|
||||||
if (mPrimaryFooterIconDrawable == null) {
|
|
||||||
DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo();
|
|
||||||
mPrimaryFooterIconDrawable = mSecurityController.getIcon(info);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mPrimaryFooterIconDrawable = null;
|
|
||||||
}
|
|
||||||
mMainHandler.post(mUpdatePrimaryIcon);
|
mMainHandler.post(mUpdatePrimaryIcon);
|
||||||
|
|
||||||
mMainHandler.post(mUpdateDisplayState);
|
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() {
|
private final Runnable mUpdatePrimaryIcon = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (mPrimaryFooterIconDrawable != null) {
|
if (mFooterIcon instanceof Icon.Loaded) {
|
||||||
mPrimaryFooterIcon.setImageDrawable(mPrimaryFooterIconDrawable);
|
mPrimaryFooterIcon.setImageDrawable(((Icon.Loaded) mFooterIcon).getDrawable());
|
||||||
} else {
|
} else if (mFooterIcon instanceof Icon.Resource) {
|
||||||
mPrimaryFooterIcon.setImageResource(mFooterIconId);
|
mPrimaryFooterIcon.setImageResource(((Icon.Resource) mFooterIcon).getRes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -890,10 +210,18 @@ class QSSecurityFooter extends ViewController<View>
|
|||||||
if (mFooterTextContent != null) {
|
if (mFooterTextContent != null) {
|
||||||
mFooterText.setText(mFooterTextContent);
|
mFooterText.setText(mFooterTextContent);
|
||||||
}
|
}
|
||||||
mView.setVisibility(mIsVisible || DEBUG_FORCE_VISIBLE ? View.VISIBLE : View.GONE);
|
mView.setVisibility(mIsVisible ? View.VISIBLE : View.GONE);
|
||||||
if (mVisibilityChangedListener != null) {
|
if (mVisibilityChangedListener != null) {
|
||||||
mVisibilityChangedListener.onVisibilityChanged(mView.getVisibility());
|
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.FontSizeUtils
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.animation.LaunchableView
|
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.QSIconView
|
||||||
import com.android.systemui.plugins.qs.QSTile
|
import com.android.systemui.plugins.qs.QSTile
|
||||||
import com.android.systemui.plugins.qs.QSTile.BooleanState
|
import com.android.systemui.plugins.qs.QSTile.BooleanState
|
||||||
@@ -138,8 +139,11 @@ open class QSTileViewImpl @JvmOverloads constructor(
|
|||||||
private var lastStateDescription: CharSequence? = null
|
private var lastStateDescription: CharSequence? = null
|
||||||
private var tileState = false
|
private var tileState = false
|
||||||
private var lastState = INVALID
|
private var lastState = INVALID
|
||||||
private var blockVisibilityChanges = false
|
private val launchableViewDelegate = LaunchableViewDelegate(
|
||||||
private var lastVisibility = View.VISIBLE
|
this,
|
||||||
|
superSetVisibility = { super.setVisibility(it) },
|
||||||
|
superSetTransitionVisibility = { super.setTransitionVisibility(it) },
|
||||||
|
)
|
||||||
|
|
||||||
private val locInScreen = IntArray(2)
|
private val locInScreen = IntArray(2)
|
||||||
|
|
||||||
@@ -343,33 +347,15 @@ open class QSTileViewImpl @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun setShouldBlockVisibilityChanges(block: Boolean) {
|
override fun setShouldBlockVisibilityChanges(block: Boolean) {
|
||||||
blockVisibilityChanges = block
|
launchableViewDelegate.setShouldBlockVisibilityChanges(block)
|
||||||
|
|
||||||
if (block) {
|
|
||||||
lastVisibility = visibility
|
|
||||||
} else {
|
|
||||||
visibility = lastVisibility
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setVisibility(visibility: Int) {
|
override fun setVisibility(visibility: Int) {
|
||||||
if (blockVisibilityChanges) {
|
launchableViewDelegate.setVisibility(visibility)
|
||||||
lastVisibility = visibility
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
super.setVisibility(visibility)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setTransitionVisibility(visibility: Int) {
|
override fun setTransitionVisibility(visibility: Int) {
|
||||||
if (blockVisibilityChanges) {
|
launchableViewDelegate.setTransitionVisibility(visibility)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accessibility
|
// 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.util.AttributeSet;
|
||||||
import android.widget.FrameLayout;
|
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
|
* A frame layout which does not have overlapping renderings commands and therefore does not need a
|
||||||
* layer when alpha is changed.
|
* 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) {
|
public AlphaOptimizedFrameLayout(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
@@ -47,4 +63,19 @@ public class AlphaOptimizedFrameLayout extends FrameLayout
|
|||||||
public boolean hasOverlappingRendering() {
|
public boolean hasOverlappingRendering() {
|
||||||
return false;
|
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).
|
* Container for image of the multi user switcher (tappable).
|
||||||
*/
|
*/
|
||||||
|
// TODO(b/242040009): Remove this file.
|
||||||
public class MultiUserSwitch extends FrameLayout {
|
public class MultiUserSwitch extends FrameLayout {
|
||||||
public MultiUserSwitch(Context context, AttributeSet attrs) {
|
public MultiUserSwitch(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import com.android.systemui.util.ViewController;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
/** View Controller for {@link MultiUserSwitch}. */
|
/** View Controller for {@link MultiUserSwitch}. */
|
||||||
|
// TODO(b/242040009): Remove this file.
|
||||||
public class MultiUserSwitchController extends ViewController<MultiUserSwitch> {
|
public class MultiUserSwitchController extends ViewController<MultiUserSwitch> {
|
||||||
private final UserManager mUserManager;
|
private final UserManager mUserManager;
|
||||||
private final UserSwitcherController mUserSwitcherController;
|
private final UserSwitcherController mUserSwitcherController;
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import android.graphics.Rect
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.testing.AndroidTestingRunner
|
import android.testing.AndroidTestingRunner
|
||||||
import android.testing.TestableLooper.RunWithLooper
|
import android.testing.TestableLooper.RunWithLooper
|
||||||
import android.util.Log
|
|
||||||
import android.view.IRemoteAnimationFinishedCallback
|
import android.view.IRemoteAnimationFinishedCallback
|
||||||
import android.view.RemoteAnimationAdapter
|
import android.view.RemoteAnimationAdapter
|
||||||
import android.view.RemoteAnimationTarget
|
import android.view.RemoteAnimationTarget
|
||||||
import android.view.SurfaceControl
|
import android.view.SurfaceControl
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
@@ -51,7 +49,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
|||||||
@Mock lateinit var listener: ActivityLaunchAnimator.Listener
|
@Mock lateinit var listener: ActivityLaunchAnimator.Listener
|
||||||
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
|
||||||
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
|
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
|
||||||
@Mock lateinit var failHandler: Log.TerribleFailureHandler
|
|
||||||
|
|
||||||
private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
|
private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
|
||||||
@get:Rule val rule = MockitoJUnit.rule()
|
@get:Rule val rule = MockitoJUnit.rule()
|
||||||
@@ -187,13 +184,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
|
|||||||
verify(controller).onLaunchAnimationStart(anyBoolean())
|
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 {
|
private fun fakeWindow(): RemoteAnimationTarget {
|
||||||
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
|
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
|
||||||
val taskInfo = ActivityManager.RunningTaskInfo()
|
val taskInfo = ActivityManager.RunningTaskInfo()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package com.android.systemui.broadcast
|
|||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -32,22 +33,27 @@ import com.android.systemui.dump.DumpManager
|
|||||||
import com.android.systemui.settings.UserTracker
|
import com.android.systemui.settings.UserTracker
|
||||||
import com.android.systemui.util.concurrency.FakeExecutor
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
import com.android.systemui.util.mockito.eq
|
import com.android.systemui.util.mockito.eq
|
||||||
|
import com.android.systemui.util.mockito.mock
|
||||||
import com.android.systemui.util.time.FakeSystemClock
|
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 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.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.ArgumentCaptor
|
import org.mockito.ArgumentCaptor
|
||||||
import org.mockito.Captor
|
import org.mockito.Captor
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.`when`
|
|
||||||
import org.mockito.Mockito.anyInt
|
import org.mockito.Mockito.anyInt
|
||||||
import org.mockito.Mockito.inOrder
|
import org.mockito.Mockito.inOrder
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.Mockito.mock
|
||||||
import org.mockito.Mockito.never
|
import org.mockito.Mockito.never
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
@RunWith(AndroidTestingRunner::class)
|
@RunWith(AndroidTestingRunner::class)
|
||||||
@TestableLooper.RunWithLooper
|
@TestableLooper.RunWithLooper
|
||||||
@@ -381,6 +387,39 @@ class BroadcastDispatcherTest : SysuiTestCase() {
|
|||||||
.clearPendingRemoval(broadcastReceiver, user1.identifier)
|
.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) {
|
private fun setUserMock(mockContext: Context, user: UserHandle) {
|
||||||
`when`(mockContext.user).thenReturn(user)
|
`when`(mockContext.user).thenReturn(user)
|
||||||
`when`(mockContext.userId).thenReturn(user.identifier)
|
`when`(mockContext.userId).thenReturn(user.identifier)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package com.android.systemui.qs;
|
package com.android.systemui.qs;
|
||||||
|
|
||||||
|
import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
@@ -34,6 +36,7 @@ import android.content.pm.PackageManager;
|
|||||||
import android.content.pm.UserInfo;
|
import android.content.pm.UserInfo;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
import android.os.UserHandle;
|
||||||
import android.provider.DeviceConfig;
|
import android.provider.DeviceConfig;
|
||||||
import android.testing.AndroidTestingRunner;
|
import android.testing.AndroidTestingRunner;
|
||||||
import android.testing.TestableLooper;
|
import android.testing.TestableLooper;
|
||||||
@@ -185,9 +188,9 @@ public class FgsManagerControllerTest extends SysuiTestCase {
|
|||||||
public void testChangesSinceLastDialog() throws RemoteException {
|
public void testChangesSinceLastDialog() throws RemoteException {
|
||||||
setUserProfiles(0);
|
setUserProfiles(0);
|
||||||
|
|
||||||
Assert.assertFalse(mFmc.getChangesSinceDialog());
|
Assert.assertFalse(mFmc.getNewChangesSinceDialogWasDismissed());
|
||||||
mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg", 0, true);
|
mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg", 0, true);
|
||||||
Assert.assertTrue(mFmc.getChangesSinceDialog());
|
Assert.assertTrue(mFmc.getNewChangesSinceDialogWasDismissed());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -222,7 +225,41 @@ public class FgsManagerControllerTest extends SysuiTestCase {
|
|||||||
Assert.assertEquals(2, mFmc.getNumRunningPackages());
|
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 {
|
FgsManagerController createFgsManagerController() throws RemoteException {
|
||||||
ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor =
|
ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor =
|
||||||
@@ -232,7 +269,7 @@ public class FgsManagerControllerTest extends SysuiTestCase {
|
|||||||
ArgumentCaptor<BroadcastReceiver> showFgsManagerReceiverArgumentCaptor =
|
ArgumentCaptor<BroadcastReceiver> showFgsManagerReceiverArgumentCaptor =
|
||||||
ArgumentCaptor.forClass(BroadcastReceiver.class);
|
ArgumentCaptor.forClass(BroadcastReceiver.class);
|
||||||
|
|
||||||
FgsManagerController result = new FgsManagerController(
|
FgsManagerController result = new FgsManagerControllerImpl(
|
||||||
mContext,
|
mContext,
|
||||||
mMainExecutor,
|
mMainExecutor,
|
||||||
mBackgroundExecutor,
|
mBackgroundExecutor,
|
||||||
|
|||||||
@@ -45,12 +45,15 @@ import com.android.systemui.R;
|
|||||||
import com.android.systemui.SysuiBaseFragmentTest;
|
import com.android.systemui.SysuiBaseFragmentTest;
|
||||||
import com.android.systemui.animation.ShadeInterpolation;
|
import com.android.systemui.animation.ShadeInterpolation;
|
||||||
import com.android.systemui.dump.DumpManager;
|
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.media.MediaHost;
|
||||||
import com.android.systemui.plugins.FalsingManager;
|
import com.android.systemui.plugins.FalsingManager;
|
||||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||||
import com.android.systemui.qs.customize.QSCustomizerController;
|
import com.android.systemui.qs.customize.QSCustomizerController;
|
||||||
import com.android.systemui.qs.dagger.QSFragmentComponent;
|
import com.android.systemui.qs.dagger.QSFragmentComponent;
|
||||||
import com.android.systemui.qs.external.TileServiceRequestController;
|
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.CommandQueue;
|
||||||
import com.android.systemui.statusbar.StatusBarState;
|
import com.android.systemui.statusbar.StatusBarState;
|
||||||
import com.android.systemui.statusbar.phone.KeyguardBypassController;
|
import com.android.systemui.statusbar.phone.KeyguardBypassController;
|
||||||
@@ -390,6 +393,8 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
|
|||||||
setUpMedia();
|
setUpMedia();
|
||||||
setUpOther();
|
setUpOther();
|
||||||
|
|
||||||
|
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
|
||||||
|
featureFlags.set(Flags.NEW_FOOTER_ACTIONS, false);
|
||||||
return new QSFragment(
|
return new QSFragment(
|
||||||
new RemoteInputQuickSettingsDisabler(
|
new RemoteInputQuickSettingsDisabler(
|
||||||
context, commandQueue, mock(ConfigurationController.class)),
|
context, commandQueue, mock(ConfigurationController.class)),
|
||||||
@@ -402,7 +407,10 @@ public class QSFragmentTest extends SysuiBaseFragmentTest {
|
|||||||
mQsComponentFactory,
|
mQsComponentFactory,
|
||||||
mock(QSFragmentDisableFlagsLogger.class),
|
mock(QSFragmentDisableFlagsLogger.class),
|
||||||
mFalsingManager,
|
mFalsingManager,
|
||||||
mock(DumpManager.class));
|
mock(DumpManager.class),
|
||||||
|
featureFlags,
|
||||||
|
mock(NewFooterActionsController.class),
|
||||||
|
mock(FooterActionsViewModel.Factory.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setUpOther() {
|
private void setUpOther() {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
private TextView mFooterText;
|
private TextView mFooterText;
|
||||||
private TestableImageView mPrimaryFooterIcon;
|
private TestableImageView mPrimaryFooterIcon;
|
||||||
private QSSecurityFooter mFooter;
|
private QSSecurityFooter mFooter;
|
||||||
|
private QSSecurityFooterUtils mFooterUtils;
|
||||||
@Mock
|
@Mock
|
||||||
private SecurityController mSecurityController;
|
private SecurityController mSecurityController;
|
||||||
@Mock
|
@Mock
|
||||||
@@ -118,13 +119,16 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
mTestableLooper = TestableLooper.get(this);
|
mTestableLooper = TestableLooper.get(this);
|
||||||
Looper looper = mTestableLooper.getLooper();
|
Looper looper = mTestableLooper.getLooper();
|
||||||
|
Handler mainHandler = new Handler(looper);
|
||||||
when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class));
|
when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class));
|
||||||
mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext)
|
mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext)
|
||||||
.replace("ImageView", TestableImageView.class)
|
.replace("ImageView", TestableImageView.class)
|
||||||
.build().inflate(R.layout.quick_settings_security_footer, null, false);
|
.build().inflate(R.layout.quick_settings_security_footer, null, false);
|
||||||
mFooter = new QSSecurityFooter(mRootView, mUserTracker, new Handler(looper),
|
mFooterUtils = new QSSecurityFooterUtils(getContext(),
|
||||||
mActivityStarter, mSecurityController, mDialogLaunchAnimator, looper,
|
getContext().getSystemService(DevicePolicyManager.class), mUserTracker,
|
||||||
mBroadcastDispatcher);
|
mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator);
|
||||||
|
mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper,
|
||||||
|
mBroadcastDispatcher, mFooterUtils);
|
||||||
mFooterText = mRootView.findViewById(R.id.footer_text);
|
mFooterText = mRootView.findViewById(R.id.footer_text);
|
||||||
mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon);
|
mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon);
|
||||||
|
|
||||||
@@ -520,7 +524,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
when(mSecurityController.isDeviceManaged()).thenReturn(true);
|
when(mSecurityController.isDeviceManaged()).thenReturn(true);
|
||||||
|
|
||||||
assertEquals(mContext.getString(R.string.monitoring_title_device_owned),
|
assertEquals(mContext.getString(R.string.monitoring_title_device_owned),
|
||||||
mFooter.getManagementTitle(MANAGING_ORGANIZATION));
|
mFooterUtils.getManagementTitle(MANAGING_ORGANIZATION));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -531,12 +535,12 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
|
|
||||||
assertEquals(mContext.getString(R.string.monitoring_title_financed_device,
|
assertEquals(mContext.getString(R.string.monitoring_title_financed_device,
|
||||||
MANAGING_ORGANIZATION),
|
MANAGING_ORGANIZATION),
|
||||||
mFooter.getManagementTitle(MANAGING_ORGANIZATION));
|
mFooterUtils.getManagementTitle(MANAGING_ORGANIZATION));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetManagementMessage_noManagement() {
|
public void testGetManagementMessage_noManagement() {
|
||||||
assertEquals(null, mFooter.getManagementMessage(
|
assertEquals(null, mFooterUtils.getManagementMessage(
|
||||||
/* isDeviceManaged= */ false, MANAGING_ORGANIZATION));
|
/* isDeviceManaged= */ false, MANAGING_ORGANIZATION));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,10 +548,10 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
public void testGetManagementMessage_deviceOwner() {
|
public void testGetManagementMessage_deviceOwner() {
|
||||||
assertEquals(mContext.getString(R.string.monitoring_description_named_management,
|
assertEquals(mContext.getString(R.string.monitoring_description_named_management,
|
||||||
MANAGING_ORGANIZATION),
|
MANAGING_ORGANIZATION),
|
||||||
mFooter.getManagementMessage(
|
mFooterUtils.getManagementMessage(
|
||||||
/* isDeviceManaged= */ true, MANAGING_ORGANIZATION));
|
/* isDeviceManaged= */ true, MANAGING_ORGANIZATION));
|
||||||
assertEquals(mContext.getString(R.string.monitoring_description_management),
|
assertEquals(mContext.getString(R.string.monitoring_description_management),
|
||||||
mFooter.getManagementMessage(
|
mFooterUtils.getManagementMessage(
|
||||||
/* isDeviceManaged= */ true,
|
/* isDeviceManaged= */ true,
|
||||||
/* organizationName= */ null));
|
/* organizationName= */ null));
|
||||||
}
|
}
|
||||||
@@ -560,68 +564,68 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
|
|
||||||
assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management,
|
assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management,
|
||||||
MANAGING_ORGANIZATION, MANAGING_ORGANIZATION),
|
MANAGING_ORGANIZATION, MANAGING_ORGANIZATION),
|
||||||
mFooter.getManagementMessage(
|
mFooterUtils.getManagementMessage(
|
||||||
/* isDeviceManaged= */ true, MANAGING_ORGANIZATION));
|
/* isDeviceManaged= */ true, MANAGING_ORGANIZATION));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetCaCertsMessage() {
|
public void testGetCaCertsMessage() {
|
||||||
assertEquals(null, mFooter.getCaCertsMessage(true, false, false));
|
assertEquals(null, mFooterUtils.getCaCertsMessage(true, false, false));
|
||||||
assertEquals(null, mFooter.getCaCertsMessage(false, false, false));
|
assertEquals(null, mFooterUtils.getCaCertsMessage(false, false, false));
|
||||||
assertEquals(mContext.getString(R.string.monitoring_description_management_ca_certificate),
|
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),
|
assertEquals(mContext.getString(R.string.monitoring_description_management_ca_certificate),
|
||||||
mFooter.getCaCertsMessage(true, false, true));
|
mFooterUtils.getCaCertsMessage(true, false, true));
|
||||||
assertEquals(mContext.getString(
|
assertEquals(mContext.getString(
|
||||||
R.string.monitoring_description_managed_profile_ca_certificate),
|
R.string.monitoring_description_managed_profile_ca_certificate),
|
||||||
mFooter.getCaCertsMessage(false, false, true));
|
mFooterUtils.getCaCertsMessage(false, false, true));
|
||||||
assertEquals(mContext.getString(
|
assertEquals(mContext.getString(
|
||||||
R.string.monitoring_description_ca_certificate),
|
R.string.monitoring_description_ca_certificate),
|
||||||
mFooter.getCaCertsMessage(false, true, false));
|
mFooterUtils.getCaCertsMessage(false, true, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetNetworkLoggingMessage() {
|
public void testGetNetworkLoggingMessage() {
|
||||||
// Test network logging message on a device with a device owner.
|
// Test network logging message on a device with a device owner.
|
||||||
// Network traffic may be monitored on the device.
|
// 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),
|
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
|
// Test network logging message on a device with a managed profile owner
|
||||||
// Network traffic may be monitored on the work profile.
|
// Network traffic may be monitored on the work profile.
|
||||||
assertEquals(null, mFooter.getNetworkLoggingMessage(false, false));
|
assertEquals(null, mFooterUtils.getNetworkLoggingMessage(false, false));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
mContext.getString(R.string.monitoring_description_managed_profile_network_logging),
|
mContext.getString(R.string.monitoring_description_managed_profile_network_logging),
|
||||||
mFooter.getNetworkLoggingMessage(false, true));
|
mFooterUtils.getNetworkLoggingMessage(false, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetVpnMessage() {
|
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,
|
assertEquals(addLink(mContext.getString(R.string.monitoring_description_two_named_vpns,
|
||||||
VPN_PACKAGE, VPN_PACKAGE_2)),
|
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,
|
assertEquals(addLink(mContext.getString(R.string.monitoring_description_two_named_vpns,
|
||||||
VPN_PACKAGE, VPN_PACKAGE_2)),
|
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,
|
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
|
||||||
VPN_PACKAGE)),
|
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,
|
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
|
||||||
VPN_PACKAGE)),
|
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,
|
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
|
||||||
VPN_PACKAGE_2)),
|
VPN_PACKAGE_2)),
|
||||||
mFooter.getVpnMessage(true, true, null, VPN_PACKAGE_2));
|
mFooterUtils.getVpnMessage(true, true, null, VPN_PACKAGE_2));
|
||||||
assertEquals(addLink(mContext.getString(
|
assertEquals(addLink(mContext.getString(
|
||||||
R.string.monitoring_description_managed_profile_named_vpn,
|
R.string.monitoring_description_managed_profile_named_vpn,
|
||||||
VPN_PACKAGE_2)),
|
VPN_PACKAGE_2)),
|
||||||
mFooter.getVpnMessage(false, true, null, VPN_PACKAGE_2));
|
mFooterUtils.getVpnMessage(false, true, null, VPN_PACKAGE_2));
|
||||||
assertEquals(addLink(mContext.getString(
|
assertEquals(addLink(mContext.getString(
|
||||||
R.string.monitoring_description_personal_profile_named_vpn,
|
R.string.monitoring_description_personal_profile_named_vpn,
|
||||||
VPN_PACKAGE)),
|
VPN_PACKAGE)),
|
||||||
mFooter.getVpnMessage(false, true, VPN_PACKAGE, null));
|
mFooterUtils.getVpnMessage(false, true, VPN_PACKAGE, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -631,19 +635,19 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
|
|
||||||
// Device Management subtitle should be shown when there is Device Management section only
|
// 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
|
// 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,
|
assertEquals(View.VISIBLE,
|
||||||
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
||||||
|
|
||||||
// If there are multiple sections, all subtitles should be shown
|
// 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,
|
assertEquals(View.VISIBLE,
|
||||||
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
||||||
assertEquals(View.VISIBLE,
|
assertEquals(View.VISIBLE,
|
||||||
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
|
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
|
||||||
|
|
||||||
// If there are multiple sections, all subtitles should be shown
|
// 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,
|
assertEquals(View.VISIBLE,
|
||||||
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
view.findViewById(R.id.device_management_subtitle).getVisibility());
|
||||||
assertEquals(View.VISIBLE,
|
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
|
// If there are multiple sections, all subtitles should be shown, event if there is no
|
||||||
// Device Management section
|
// Device Management section
|
||||||
mFooter.configSubtitleVisibility(false, true, true, true, view);
|
mFooterUtils.configSubtitleVisibility(false, true, true, true, view);
|
||||||
assertEquals(View.VISIBLE,
|
assertEquals(View.VISIBLE,
|
||||||
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
|
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
|
||||||
assertEquals(View.VISIBLE,
|
assertEquals(View.VISIBLE,
|
||||||
@@ -664,13 +668,13 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
view.findViewById(R.id.vpn_subtitle).getVisibility());
|
view.findViewById(R.id.vpn_subtitle).getVisibility());
|
||||||
|
|
||||||
// If there is only 1 section, the title should be hidden
|
// 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,
|
assertEquals(View.GONE,
|
||||||
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
|
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,
|
assertEquals(View.GONE,
|
||||||
view.findViewById(R.id.network_logging_subtitle).getVisibility());
|
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,
|
assertEquals(View.GONE,
|
||||||
view.findViewById(R.id.vpn_subtitle).getVisibility());
|
view.findViewById(R.id.vpn_subtitle).getVisibility());
|
||||||
}
|
}
|
||||||
@@ -690,6 +694,9 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParentalControls() {
|
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);
|
when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
|
||||||
|
|
||||||
Drawable testDrawable = new VectorDrawable();
|
Drawable testDrawable = new VectorDrawable();
|
||||||
@@ -719,7 +726,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
|
when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
|
||||||
when(mSecurityController.getLabel(any())).thenReturn(PARENTAL_CONTROLS_LABEL);
|
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);
|
TextView textView = (TextView) view.findViewById(R.id.parental_controls_title);
|
||||||
assertEquals(PARENTAL_CONTROLS_LABEL, textView.getText());
|
assertEquals(PARENTAL_CONTROLS_LABEL, textView.getText());
|
||||||
}
|
}
|
||||||
@@ -742,7 +749,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
|
when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
|
||||||
.thenReturn(DEVICE_OWNER_TYPE_FINANCED);
|
.thenReturn(DEVICE_OWNER_TYPE_FINANCED);
|
||||||
|
|
||||||
View view = mFooter.createDialogView();
|
View view = mFooterUtils.createDialogView();
|
||||||
|
|
||||||
TextView managementSubtitle = view.findViewById(R.id.device_management_subtitle);
|
TextView managementSubtitle = view.findViewById(R.id.device_management_subtitle);
|
||||||
assertEquals(View.VISIBLE, managementSubtitle.getVisibility());
|
assertEquals(View.VISIBLE, managementSubtitle.getVisibility());
|
||||||
@@ -753,7 +760,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management,
|
assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management,
|
||||||
MANAGING_ORGANIZATION, MANAGING_ORGANIZATION), managementMessage.getText());
|
MANAGING_ORGANIZATION, MANAGING_ORGANIZATION), managementMessage.getText());
|
||||||
assertEquals(mContext.getString(R.string.monitoring_button_view_policies),
|
assertEquals(mContext.getString(R.string.monitoring_button_view_policies),
|
||||||
mFooter.getSettingsButton());
|
mFooterUtils.getSettingsButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -773,7 +780,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
AlertDialog dialog = dialogCaptor.getValue();
|
AlertDialog dialog = dialogCaptor.getValue();
|
||||||
dialog.create();
|
dialog.create();
|
||||||
|
|
||||||
assertEquals(mFooter.getSettingsButton(),
|
assertEquals(mFooterUtils.getSettingsButton(),
|
||||||
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getText());
|
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getText());
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
@@ -816,8 +823,8 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG));
|
new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG));
|
||||||
mTestableLooper.processAllMessages();
|
mTestableLooper.processAllMessages();
|
||||||
|
|
||||||
assertTrue(mFooter.getDialog().isShowing());
|
assertTrue(mFooterUtils.getDialog().isShowing());
|
||||||
mFooter.getDialog().dismiss();
|
mFooterUtils.getDialog().dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharSequence addLink(CharSequence description) {
|
private CharSequence addLink(CharSequence description) {
|
||||||
@@ -825,7 +832,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
|
|||||||
message.append(description);
|
message.append(description);
|
||||||
message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator));
|
message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator));
|
||||||
message.append(mContext.getString(R.string.monitoring_description_vpn_settings),
|
message.append(mContext.getString(R.string.monitoring_description_vpn_settings),
|
||||||
mFooter.new VpnSpan(), 0);
|
mFooterUtils.new VpnSpan(), 0);
|
||||||
return message;
|
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)
|
stringFlags.put(flag.id, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun isEnabled(flag: UnreleasedFlag): Boolean = requireBooleanValue(flag.id)
|
override fun isEnabled(flag: UnreleasedFlag): Boolean = requireBooleanValue(flag.id)
|
||||||
|
|
||||||
override fun isEnabled(flag: ReleasedFlag): 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