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:
Jordan Demeulenaere
2022-08-26 06:47:09 +00:00
committed by Android (Google) Code Review
66 changed files with 4733 additions and 883 deletions

View File

@@ -548,6 +548,12 @@ public final class SystemUiDeviceConfigFlags {
*/
public static final String TASK_MANAGER_SHOW_FOOTER_DOT = "task_manager_show_footer_dot";
/**
* (boolean) Whether the task manager should show a stop button if the app is allowlisted
* by the user.
*/
public static final String TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS =
"show_stop_button_for_user_allowlisted_apps";
/**
* (boolean) Whether the clipboard overlay is enabled.

View File

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

View File

@@ -230,6 +230,7 @@ android_library {
libs: [
"android.test.runner",
"android.test.base",
"android.test.mock",
],
kotlincflags: ["-Xjvm-default=enable"],
aaptflags: [

View File

@@ -311,8 +311,7 @@ class ActivityLaunchAnimator(
@JvmStatic
fun fromView(view: View, cujType: Int? = null): Controller? {
if (view.parent !is ViewGroup) {
// TODO(b/192194319): Throw instead of just logging.
Log.wtf(
Log.e(
TAG,
"Skipping animation as view $view is not attached to a ViewGroup",
Exception()

View File

@@ -113,6 +113,19 @@ constructor(
}
val animateFrom = animatedParent?.dialogContentWithBackground ?: view
if (animatedParent == null && animateFrom !is LaunchableView) {
// Make sure the View we launch from implements LaunchableView to avoid visibility
// issues. Given that we don't own dialog decorViews so we can't enforce it for launches
// from a dialog.
// TODO(b/243636422): Throw instead of logging to enforce this.
Log.w(
TAG,
"A dialog was launched from a View that does not implement LaunchableView. This " +
"can lead to subtle bugs where the visibility of the View we are " +
"launching from is not what we expected."
)
}
// Make sure we don't run the launch animation from the same view twice at the same time.
if (animateFrom.getTag(TAG_LAUNCH_ANIMATION_RUNNING) != null) {
Log.e(TAG, "Not running dialog launch animation as there is already one running")
@@ -156,9 +169,14 @@ constructor(
openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
?: throw IllegalStateException(
"The animateFrom dialog was not animated using " +
"DialogLaunchAnimator.showFrom(View|Dialog)")
"DialogLaunchAnimator.showFrom(View|Dialog)"
)
showFromView(
dialog, view, animateBackgroundBoundsChange = animateBackgroundBoundsChange, cuj = cuj)
dialog,
view,
animateBackgroundBoundsChange = animateBackgroundBoundsChange,
cuj = cuj
)
}
/**
@@ -197,7 +215,7 @@ constructor(
// bouncer.
if (
!dialog.isShowing ||
(!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
(!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock())
) {
return null
}
@@ -556,11 +574,12 @@ private class AnimatedDialog(
window.setDecorFitsSystemWindows(false)
val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
val type = if (wasFittingNavigationBars) {
WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars()
} else {
WindowInsets.Type.displayCutout()
}
val type =
if (wasFittingNavigationBars) {
WindowInsets.Type.displayCutout() or WindowInsets.Type.navigationBars()
} else {
WindowInsets.Type.displayCutout()
}
val insets = windowInsets.getInsets(type)
view.setPadding(insets.left, insets.top, insets.right, insets.bottom)

View File

@@ -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)
}
}
}
}
}

View File

@@ -16,15 +16,79 @@
package com.android.systemui.animation
import android.view.View
/** A view that can expand/launch into an app or a dialog. */
interface LaunchableView {
/**
* Set whether this view should block/prevent all visibility changes. This ensures that this
* view remains invisible during the launch animation given that it is ghosted and already drawn
* Set whether this view should block/postpone all visibility changes. This ensures that this
* view:
* - remains invisible during the launch animation given that it is ghosted and already drawn
* somewhere else.
* - remains invisible as long as a dialog expanded from it is shown.
* - restores its expected visibility once the dialog expanded from it is dismissed.
*
* Note that when this is set to true, both the [normal][android.view.View.setVisibility] and
* [transition][android.view.View.setTransitionVisibility] visibility changes must be blocked.
*
* @param block whether we should block/postpone all calls to `setVisibility` and
* `setTransitionVisibility`.
*/
fun setShouldBlockVisibilityChanges(block: Boolean)
}
/** A delegate that can be used by views to make the implementation of [LaunchableView] easier. */
class LaunchableViewDelegate(
private val view: View,
/**
* The lambda that should set the actual visibility of [view], usually by calling
* super.setVisibility(visibility).
*/
private val superSetVisibility: (Int) -> Unit,
/**
* The lambda that should set the actual transition visibility of [view], usually by calling
* super.setTransitionVisibility(visibility).
*/
private val superSetTransitionVisibility: (Int) -> Unit,
) {
private var blockVisibilityChanges = false
private var lastVisibility = view.visibility
/** Call this when [LaunchableView.setShouldBlockVisibilityChanges] is called. */
fun setShouldBlockVisibilityChanges(block: Boolean) {
if (block == blockVisibilityChanges) {
return
}
blockVisibilityChanges = block
if (block) {
lastVisibility = view.visibility
} else {
superSetVisibility(lastVisibility)
}
}
/** Call this when [View.setVisibility] is called. */
fun setVisibility(visibility: Int) {
if (blockVisibilityChanges) {
lastVisibility = visibility
return
}
superSetVisibility(visibility)
}
/** Call this when [View.setTransitionVisibility] is called. */
fun setTransitionVisibility(visibility: Int) {
if (blockVisibilityChanges) {
// View.setTransitionVisibility just sets the visibility flag, so we don't have to save
// the transition visibility separately from the normal visibility.
lastVisibility = visibility
return
}
superSetTransitionVisibility(visibility)
}
}

View File

@@ -54,6 +54,11 @@ android_library {
"testables",
"truth-prebuilt",
"androidx.test.uiautomator",
"kotlinx_coroutines_test",
],
libs: [
"android.test.mock",
],
kotlincflags: ["-Xjvm-default=all"],

View File

@@ -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)
}
}

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/qs_footer_action_button_size"
android:layout_height="@dimen/qs_footer_action_button_size"
android:visibility="gone">
<ImageView
android:id="@+id/icon"
android:layout_width="@dimen/qs_footer_icon_size"
android:layout_height="@dimen/qs_footer_icon_size"
android:layout_gravity="center"
android:scaleType="centerInside" />
</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2022 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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>

View File

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

View File

@@ -380,6 +380,7 @@
<!-- (48dp - 40dp) / 2 -->
<dimen name="qs_footer_action_inset">4dp</dimen>
<dimen name="qs_footer_actions_top_padding">8dp</dimen>
<dimen name="qs_footer_actions_bottom_padding">4dp</dimen>
<dimen name="qs_footer_action_inset_negative">-4dp</dimen>

View File

@@ -31,6 +31,8 @@ import android.util.SparseArray
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
@@ -38,6 +40,8 @@ import com.android.systemui.settings.UserTracker
import java.io.PrintWriter
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
data class ReceiverData(
val receiver: BroadcastReceiver,
@@ -153,6 +157,55 @@ open class BroadcastDispatcher @Inject constructor(
.sendToTarget()
}
/**
* Returns a [Flow] that, when collected, emits a new value whenever a broadcast matching
* [filter] is received. The value will be computed from the intent and the registered receiver
* using [map].
*
* @see registerReceiver
*/
@JvmOverloads
fun <T> broadcastFlow(
filter: IntentFilter,
user: UserHandle? = null,
@Context.RegisterReceiverFlags flags: Int = Context.RECEIVER_EXPORTED,
permission: String? = null,
map: (Intent, BroadcastReceiver) -> T,
): Flow<T> = conflatedCallbackFlow {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
trySendWithFailureLogging(map(intent, this), TAG)
}
}
registerReceiver(
receiver,
filter,
bgExecutor,
user,
flags,
permission,
)
awaitClose {
unregisterReceiver(receiver)
}
}
/**
* Returns a [Flow] that, when collected, emits `Unit` whenever a broadcast matching [filter] is
* received.
*
* @see registerReceiver
*/
@JvmOverloads
fun broadcastFlow(
filter: IntentFilter,
user: UserHandle? = null,
@Context.RegisterReceiverFlags flags: Int = Context.RECEIVER_EXPORTED,
permission: String? = null,
): Flow<Unit> = broadcastFlow(filter, user, flags, permission) { _, _ -> Unit }
private fun checkFilter(filter: IntentFilter) {
val sb = StringBuilder()
if (filter.countActions() == 0) sb.append("Filter must contain at least one action. ")

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.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()
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.common.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()
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.common.ui.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)
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

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

View File

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

View File

@@ -40,11 +40,13 @@ import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_FOOTER_DOT
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.Dumpable
import com.android.systemui.R
@@ -66,9 +68,73 @@ import java.util.Objects
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlin.math.max
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** A controller for the dealing with services running in the foreground. */
interface FgsManagerController {
/** Whether the TaskManager (and therefore this controller) is actually available. */
val isAvailable: StateFlow<Boolean>
/** The number of packages with a service running in the foreground. */
val numRunningPackages: Int
/**
* Whether there were new changes to the foreground services since the last [shown][showDialog]
* dialog was dismissed.
*/
val newChangesSinceDialogWasDismissed: Boolean
/**
* Whether we should show a dot to indicate when [newChangesSinceDialogWasDismissed] is true.
*/
val showFooterDot: StateFlow<Boolean>
/**
* Initialize this controller. This should be called once, before this controller is used for
* the first time.
*/
fun init()
/**
* Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if
* it's not `null`.
*/
fun showDialog(viewLaunchedFrom: View?)
/** Add a [OnNumberOfPackagesChangedListener]. */
fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
/** Remove a [OnNumberOfPackagesChangedListener]. */
fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
/** Add a [OnDialogDismissedListener]. */
fun addOnDialogDismissedListener(listener: OnDialogDismissedListener)
/** Remove a [OnDialogDismissedListener]. */
fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener)
/** Whether we should update the footer visibility. */
// TODO(b/242040009): Remove this.
fun shouldUpdateFooterVisibility(): Boolean
@VisibleForTesting
fun visibleButtonsCount(): Int
interface OnNumberOfPackagesChangedListener {
/** Called when [numRunningPackages] changed. */
fun onNumberOfPackagesChanged(numPackages: Int)
}
interface OnDialogDismissedListener {
/** Called when a dialog shown using [showDialog] was dismissed. */
fun onDialogDismissed()
}
}
@SysUISingleton
class FgsManagerController @Inject constructor(
class FgsManagerControllerImpl @Inject constructor(
private val context: Context,
@Main private val mainExecutor: Executor,
@Background private val backgroundExecutor: Executor,
@@ -80,22 +146,32 @@ class FgsManagerController @Inject constructor(
private val dialogLaunchAnimator: DialogLaunchAnimator,
private val broadcastDispatcher: BroadcastDispatcher,
private val dumpManager: DumpManager
) : IForegroundServiceObserver.Stub(), Dumpable {
) : IForegroundServiceObserver.Stub(), Dumpable, FgsManagerController {
companion object {
private const val INTERACTION_JANK_TAG = "active_background_apps"
private val LOG_TAG = FgsManagerController::class.java.simpleName
private const val DEFAULT_TASK_MANAGER_ENABLED = true
private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false
private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true
}
var changesSinceDialog = false
override var newChangesSinceDialogWasDismissed = false
private set
var isAvailable = false
private set
var showFooterDot = false
private set
val _isAvailable = MutableStateFlow(false)
override val isAvailable: StateFlow<Boolean> = _isAvailable.asStateFlow()
val _showFooterDot = MutableStateFlow(false)
override val showFooterDot: StateFlow<Boolean> = _showFooterDot.asStateFlow()
private var showStopBtnForUserAllowlistedApps = false
override val numRunningPackages: Int
get() {
synchronized(lock) {
return getNumVisiblePackagesLocked()
}
}
private val lock = Any()
@@ -133,15 +209,7 @@ class FgsManagerController @Inject constructor(
}
}
interface OnNumberOfPackagesChangedListener {
fun onNumberOfPackagesChanged(numPackages: Int)
}
interface OnDialogDismissedListener {
fun onDialogDismissed()
}
fun init() {
override fun init() {
synchronized(lock) {
if (initialized) {
return
@@ -160,19 +228,26 @@ class FgsManagerController @Inject constructor(
NAMESPACE_SYSTEMUI,
backgroundExecutor
) {
isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable)
showFooterDot =
it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, showFooterDot)
_isAvailable.value = it.getBoolean(TASK_MANAGER_ENABLED, _isAvailable.value)
_showFooterDot.value =
it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, _showFooterDot.value)
showStopBtnForUserAllowlistedApps = it.getBoolean(
TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
showStopBtnForUserAllowlistedApps)
}
isAvailable = deviceConfigProxy.getBoolean(
_isAvailable.value = deviceConfigProxy.getBoolean(
NAMESPACE_SYSTEMUI,
TASK_MANAGER_ENABLED, DEFAULT_TASK_MANAGER_ENABLED
)
showFooterDot = deviceConfigProxy.getBoolean(
_showFooterDot.value = deviceConfigProxy.getBoolean(
NAMESPACE_SYSTEMUI,
TASK_MANAGER_SHOW_FOOTER_DOT, DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT
)
showStopBtnForUserAllowlistedApps = deviceConfigProxy.getBoolean(
NAMESPACE_SYSTEMUI,
TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS)
dumpManager.registerDumpable(this)
@@ -220,42 +295,45 @@ class FgsManagerController @Inject constructor(
}
@GuardedBy("lock")
val onNumberOfPackagesChangedListeners: MutableSet<OnNumberOfPackagesChangedListener> =
mutableSetOf()
private val onNumberOfPackagesChangedListeners =
mutableSetOf<FgsManagerController.OnNumberOfPackagesChangedListener>()
@GuardedBy("lock")
val onDialogDismissedListeners: MutableSet<OnDialogDismissedListener> = mutableSetOf()
private val onDialogDismissedListeners =
mutableSetOf<FgsManagerController.OnDialogDismissedListener>()
fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
override fun addOnNumberOfPackagesChangedListener(
listener: FgsManagerController.OnNumberOfPackagesChangedListener
) {
synchronized(lock) {
onNumberOfPackagesChangedListeners.add(listener)
}
}
fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
override fun removeOnNumberOfPackagesChangedListener(
listener: FgsManagerController.OnNumberOfPackagesChangedListener
) {
synchronized(lock) {
onNumberOfPackagesChangedListeners.remove(listener)
}
}
fun addOnDialogDismissedListener(listener: OnDialogDismissedListener) {
override fun addOnDialogDismissedListener(
listener: FgsManagerController.OnDialogDismissedListener
) {
synchronized(lock) {
onDialogDismissedListeners.add(listener)
}
}
fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener) {
override fun removeOnDialogDismissedListener(
listener: FgsManagerController.OnDialogDismissedListener
) {
synchronized(lock) {
onDialogDismissedListeners.remove(listener)
}
}
fun getNumRunningPackages(): Int {
synchronized(lock) {
return getNumVisiblePackagesLocked()
}
}
private fun getNumVisiblePackagesLocked(): Int {
return runningServiceTokens.keys.count {
it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId)
@@ -266,7 +344,7 @@ class FgsManagerController @Inject constructor(
val num = getNumVisiblePackagesLocked()
if (num != lastNumberOfVisiblePackages) {
lastNumberOfVisiblePackages = num
changesSinceDialog = true
newChangesSinceDialogWasDismissed = true
onNumberOfPackagesChangedListeners.forEach {
backgroundExecutor.execute {
it.onNumberOfPackagesChanged(num)
@@ -275,9 +353,21 @@ class FgsManagerController @Inject constructor(
}
}
fun shouldUpdateFooterVisibility() = dialog == null
override fun visibleButtonsCount(): Int {
synchronized(lock) {
return getNumVisibleButtonsLocked()
}
}
fun showDialog(viewLaunchedFrom: View?) {
private fun getNumVisibleButtonsLocked(): Int {
return runningServiceTokens.keys.count {
it.uiControl != UIControl.HIDE_BUTTON && currentProfileIds.contains(it.userId)
}
}
override fun shouldUpdateFooterVisibility() = dialog == null
override fun showDialog(viewLaunchedFrom: View?) {
synchronized(lock) {
if (dialog == null) {
@@ -302,7 +392,7 @@ class FgsManagerController @Inject constructor(
this.dialog = dialog
dialog.setOnDismissListener {
changesSinceDialog = false
newChangesSinceDialogWasDismissed = false
synchronized(lock) {
this.dialog = null
updateAppItemsLocked()
@@ -505,6 +595,13 @@ class FgsManagerController @Inject constructor(
PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI,
PowerExemptionManager.REASON_ROLE_DIALER,
PowerExemptionManager.REASON_SYSTEM_MODULE -> UIControl.HIDE_BUTTON
PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE ->
if (showStopBtnForUserAllowlistedApps) {
UIControl.NORMAL
} else {
UIControl.HIDE_BUTTON
}
else -> UIControl.NORMAL
}
uiControlInitialized = true
@@ -623,7 +720,7 @@ class FgsManagerController @Inject constructor(
val pw = IndentingPrintWriter(printwriter)
synchronized(lock) {
pw.println("current user profiles = $currentProfileIds")
pw.println("changesSinceDialog=$changesSinceDialog")
pw.println("newChangesSinceDialogWasShown=$newChangesSinceDialogWasDismissed")
pw.println("Running service tokens: [")
pw.indentIfPossible {
runningServiceTokens.forEach { (userPackage, startTimeAndTokens) ->

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.qs
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
/** Controller for the footer actions. This manages the initialization of its dependencies. */
@SysUISingleton
class NewFooterActionsController
@Inject
// TODO(b/242040009): Rename this to FooterActionsController.
constructor(
private val fgsManagerController: FgsManagerController,
) {
fun init() {
fgsManagerController.init()
}
}

View File

@@ -41,6 +41,7 @@ import javax.inject.Named;
/**
* Footer entry point for the foreground service manager
*/
// TODO(b/242040009): Remove this file.
@QSScope
public class QSFgsManagerFooter implements View.OnClickListener,
FgsManagerController.OnDialogDismissedListener,
@@ -149,9 +150,11 @@ public class QSFgsManagerFooter implements View.OnClickListener,
mNumberView.setContentDescription(text);
if (mFgsManagerController.shouldUpdateFooterVisibility()) {
mRootView.setVisibility(mNumPackages > 0
&& mFgsManagerController.isAvailable() ? View.VISIBLE : View.GONE);
int dotVis = mFgsManagerController.getShowFooterDot()
&& mFgsManagerController.getChangesSinceDialog() ? View.VISIBLE : View.GONE;
&& mFgsManagerController.isAvailable().getValue() ? View.VISIBLE
: View.GONE);
int dotVis = mFgsManagerController.getShowFooterDot().getValue()
&& mFgsManagerController.getNewChangesSinceDialogWasDismissed()
? View.VISIBLE : View.GONE;
mDotView.setVisibility(dotVis);
mCollapsedDotView.setVisibility(dotVis);
if (mVisibilityChangedListener != null) {

View File

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

View File

@@ -15,126 +15,66 @@
*/
package com.android.systemui.qs;
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_CA_CERT;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NAMED_VPN;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NETWORK;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TITLE;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_CA_CERT_SUBTITLE;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_NETWORK_SUBTITLE;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_VPN_SUBTITLE;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_NAMED_MANAGEMENT;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_VIEW_POLICIES;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_CA_CERT;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NAMED_VPN;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_WORK_PROFILE_NETWORK;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MONITORING;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_MULTIPLE_VPNS;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_MANAGEMENT_NAMED_VPN;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MONITORING;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_MANAGEMENT_NAMED_VPN;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_NAMED_WORK_PROFILE_MONITORING;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_PERSONAL_PROFILE_NAMED_VPN;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_MONITORING;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NAMED_VPN;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_MSG_WORK_PROFILE_NETWORK;
import static com.android.systemui.qs.dagger.QSFragmentModule.QS_SECURITY_FOOTER_VIEW;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.admin.DeviceAdminInfo;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.util.FrameworkStatsLog;
import com.android.systemui.FontSizeUtils;
import com.android.systemui.R;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.common.shared.model.Icon;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.dagger.QSScope;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig;
import com.android.systemui.security.data.model.SecurityModel;
import com.android.systemui.statusbar.policy.SecurityController;
import com.android.systemui.util.ViewController;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import javax.inject.Inject;
import javax.inject.Named;
/** ViewController for the footer actions. */
// TODO(b/242040009): Remove this class.
@QSScope
class QSSecurityFooter extends ViewController<View>
implements OnClickListener, DialogInterface.OnClickListener,
VisibilityChangedDispatcher {
public class QSSecurityFooter extends ViewController<View>
implements OnClickListener, VisibilityChangedDispatcher {
protected static final String TAG = "QSSecurityFooter";
protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean DEBUG_FORCE_VISIBLE = false;
private static final String INTERACTION_JANK_TAG = "managed_device_info";
private final TextView mFooterText;
private final ImageView mPrimaryFooterIcon;
private Context mContext;
private final DevicePolicyManager mDpm;
private final Callback mCallback = new Callback();
private final SecurityController mSecurityController;
private final ActivityStarter mActivityStarter;
private final Handler mMainHandler;
private final UserTracker mUserTracker;
private final DialogLaunchAnimator mDialogLaunchAnimator;
private final BroadcastDispatcher mBroadcastDispatcher;
private final QSSecurityFooterUtils mQSSecurityFooterUtils;
private final AtomicBoolean mShouldUseSettingsButton = new AtomicBoolean(false);
private AlertDialog mDialog;
protected H mHandler;
private boolean mIsVisible;
private boolean mIsClickable;
@Nullable
private CharSequence mFooterTextContent = null;
private int mFooterIconId;
@Nullable
private Drawable mPrimaryFooterIconDrawable;
private Icon mFooterIcon;
@Nullable
private VisibilityChangedDispatcher.OnVisibilityChangedListener mVisibilityChangedListener;
@@ -149,82 +89,21 @@ class QSSecurityFooter extends ViewController<View>
}
};
private Supplier<String> mManagementTitleSupplier = () ->
mContext == null ? null : mContext.getString(R.string.monitoring_title_device_owned);
private Supplier<String> mManagementMessageSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.quick_settings_disclosure_management);
private Supplier<String> mManagementMonitoringStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.quick_settings_disclosure_management_monitoring);
private Supplier<String> mManagementMultipleVpnStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.quick_settings_disclosure_management_vpns);
private Supplier<String> mWorkProfileMonitoringStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.quick_settings_disclosure_managed_profile_monitoring);
private Supplier<String> mWorkProfileNetworkStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.quick_settings_disclosure_managed_profile_network_activity);
private Supplier<String> mMonitoringSubtitleCaCertStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.monitoring_subtitle_ca_certificate);
private Supplier<String> mMonitoringSubtitleNetworkStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.monitoring_subtitle_network_logging);
private Supplier<String> mMonitoringSubtitleVpnStringSupplier = () ->
mContext == null ? null : mContext.getString(R.string.monitoring_subtitle_vpn);
private Supplier<String> mViewPoliciesButtonStringSupplier = () ->
mContext == null ? null : mContext.getString(R.string.monitoring_button_view_policies);
private Supplier<String> mManagementDialogStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.monitoring_description_management);
private Supplier<String> mManagementDialogCaCertStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.monitoring_description_management_ca_certificate);
private Supplier<String> mWorkProfileDialogCaCertStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.monitoring_description_managed_profile_ca_certificate);
private Supplier<String> mManagementDialogNetworkStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.monitoring_description_management_network_logging);
private Supplier<String> mWorkProfileDialogNetworkStringSupplier = () ->
mContext == null ? null : mContext.getString(
R.string.monitoring_description_managed_profile_network_logging);
@Inject
QSSecurityFooter(@Named(QS_SECURITY_FOOTER_VIEW) View rootView,
UserTracker userTracker, @Main Handler mainHandler,
ActivityStarter activityStarter, SecurityController securityController,
DialogLaunchAnimator dialogLaunchAnimator, @Background Looper bgLooper,
BroadcastDispatcher broadcastDispatcher) {
@Main Handler mainHandler, SecurityController securityController,
@Background Looper bgLooper, BroadcastDispatcher broadcastDispatcher,
QSSecurityFooterUtils qSSecurityFooterUtils) {
super(rootView);
mFooterText = mView.findViewById(R.id.footer_text);
mPrimaryFooterIcon = mView.findViewById(R.id.primary_footer_icon);
mFooterIconId = R.drawable.ic_info_outline;
mFooterIcon = new Icon.Resource(R.drawable.ic_info_outline);
mContext = rootView.getContext();
mDpm = rootView.getContext().getSystemService(DevicePolicyManager.class);
mMainHandler = mainHandler;
mActivityStarter = activityStarter;
mSecurityController = securityController;
mMainHandler = mainHandler;
mHandler = new H(bgLooper);
mUserTracker = userTracker;
mDialogLaunchAnimator = dialogLaunchAnimator;
mBroadcastDispatcher = broadcastDispatcher;
mQSSecurityFooterUtils = qSSecurityFooterUtils;
}
@Override
@@ -287,8 +166,9 @@ class QSSecurityFooter extends ViewController<View>
.write();
}
// TODO(b/242040009): Remove this.
public void showDeviceMonitoringDialog() {
createDialog();
mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView);
}
public void refreshState() {
@@ -296,590 +176,30 @@ class QSSecurityFooter extends ViewController<View>
}
private void handleRefreshState() {
final boolean isDeviceManaged = mSecurityController.isDeviceManaged();
final UserInfo currentUser = mUserTracker.getUserInfo();
final boolean isDemoDevice = UserManager.isDeviceInDemoMode(mContext) && currentUser != null
&& currentUser.isDemo();
final boolean hasWorkProfile = mSecurityController.hasWorkProfile();
final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser();
final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile();
final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled();
final String vpnName = mSecurityController.getPrimaryVpnName();
final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName();
final CharSequence organizationName = mSecurityController.getDeviceOwnerOrganizationName();
final CharSequence workProfileOrganizationName =
mSecurityController.getWorkProfileOrganizationName();
final boolean isProfileOwnerOfOrganizationOwnedDevice =
mSecurityController.isProfileOwnerOfOrganizationOwnedDevice();
final boolean isParentalControlsEnabled = mSecurityController.isParentalControlsEnabled();
final boolean isWorkProfileOn = mSecurityController.isWorkProfileOn();
final boolean hasDisclosableWorkProfilePolicy = hasCACertsInWorkProfile
|| vpnNameWorkProfile != null || (hasWorkProfile && isNetworkLoggingEnabled);
// Update visibility of footer
mIsVisible = (isDeviceManaged && !isDemoDevice)
|| hasCACerts
|| vpnName != null
|| isProfileOwnerOfOrganizationOwnedDevice
|| isParentalControlsEnabled
|| (hasDisclosableWorkProfilePolicy && isWorkProfileOn);
// Update the view to be untappable if the device is an organization-owned device with a
// managed profile and there is either:
// a) no policy set which requires a privacy disclosure.
// b) a specific work policy set but the work profile is turned off.
if (mIsVisible && isProfileOwnerOfOrganizationOwnedDevice
&& (!hasDisclosableWorkProfilePolicy || !isWorkProfileOn)) {
mView.setClickable(false);
mView.findViewById(R.id.footer_icon).setVisibility(View.GONE);
SecurityModel securityModel = SecurityModel.create(mSecurityController);
SecurityButtonConfig buttonConfig = mQSSecurityFooterUtils.getButtonConfig(securityModel);
if (buttonConfig == null) {
mIsVisible = false;
} else {
mView.setClickable(true);
mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE);
}
// Update the string
mFooterTextContent = getFooterText(isDeviceManaged, hasWorkProfile,
hasCACerts, hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName,
vpnNameWorkProfile, organizationName, workProfileOrganizationName,
isProfileOwnerOfOrganizationOwnedDevice, isParentalControlsEnabled,
isWorkProfileOn);
// Update the icon
int footerIconId = R.drawable.ic_info_outline;
if (vpnName != null || vpnNameWorkProfile != null) {
if (mSecurityController.isVpnBranded()) {
footerIconId = R.drawable.stat_sys_branded_vpn;
} else {
footerIconId = R.drawable.stat_sys_vpn_ic;
}
}
if (mFooterIconId != footerIconId) {
mFooterIconId = footerIconId;
mIsVisible = true;
mIsClickable = buttonConfig.isClickable();
mFooterTextContent = buttonConfig.getText();
mFooterIcon = buttonConfig.getIcon();
}
// Update the primary icon
if (isParentalControlsEnabled) {
if (mPrimaryFooterIconDrawable == null) {
DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo();
mPrimaryFooterIconDrawable = mSecurityController.getIcon(info);
}
} else {
mPrimaryFooterIconDrawable = null;
}
// Update the UI.
mMainHandler.post(mUpdatePrimaryIcon);
mMainHandler.post(mUpdateDisplayState);
}
@Nullable
protected CharSequence getFooterText(boolean isDeviceManaged, boolean hasWorkProfile,
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
String vpnName, String vpnNameWorkProfile, CharSequence organizationName,
CharSequence workProfileOrganizationName,
boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isParentalControlsEnabled,
boolean isWorkProfileOn) {
if (isParentalControlsEnabled) {
return mContext.getString(R.string.quick_settings_disclosure_parental_controls);
}
if (isDeviceManaged || DEBUG_FORCE_VISIBLE) {
return getManagedDeviceFooterText(hasCACerts, hasCACertsInWorkProfile,
isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile, organizationName);
}
return getManagedAndPersonalProfileFooterText(hasWorkProfile, hasCACerts,
hasCACertsInWorkProfile, isNetworkLoggingEnabled, vpnName, vpnNameWorkProfile,
workProfileOrganizationName, isProfileOwnerOfOrganizationOwnedDevice,
isWorkProfileOn);
}
private String getManagedDeviceFooterText(
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
String vpnName, String vpnNameWorkProfile, CharSequence organizationName) {
if (hasCACerts || hasCACertsInWorkProfile || isNetworkLoggingEnabled) {
return getManagedDeviceMonitoringText(organizationName);
}
if (vpnName != null || vpnNameWorkProfile != null) {
return getManagedDeviceVpnText(vpnName, vpnNameWorkProfile, organizationName);
}
return getMangedDeviceGeneralText(organizationName);
}
private String getManagedDeviceMonitoringText(CharSequence organizationName) {
if (organizationName == null) {
return mDpm.getResources().getString(
QS_MSG_MANAGEMENT_MONITORING, mManagementMonitoringStringSupplier);
}
return mDpm.getResources().getString(
QS_MSG_NAMED_MANAGEMENT_MONITORING,
() -> mContext.getString(
R.string.quick_settings_disclosure_named_management_monitoring,
organizationName),
organizationName);
}
private String getManagedDeviceVpnText(
String vpnName, String vpnNameWorkProfile, CharSequence organizationName) {
if (vpnName != null && vpnNameWorkProfile != null) {
if (organizationName == null) {
return mDpm.getResources().getString(
QS_MSG_MANAGEMENT_MULTIPLE_VPNS, mManagementMultipleVpnStringSupplier);
}
return mDpm.getResources().getString(
QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS,
() -> mContext.getString(
R.string.quick_settings_disclosure_named_management_vpns,
organizationName),
organizationName);
}
String name = vpnName != null ? vpnName : vpnNameWorkProfile;
if (organizationName == null) {
return mDpm.getResources().getString(
QS_MSG_MANAGEMENT_NAMED_VPN,
() -> mContext.getString(
R.string.quick_settings_disclosure_management_named_vpn,
name),
name);
}
return mDpm.getResources().getString(
QS_MSG_NAMED_MANAGEMENT_NAMED_VPN,
() -> mContext.getString(
R.string.quick_settings_disclosure_named_management_named_vpn,
organizationName,
name),
organizationName,
name);
}
private String getMangedDeviceGeneralText(CharSequence organizationName) {
if (organizationName == null) {
return mDpm.getResources().getString(QS_MSG_MANAGEMENT, mManagementMessageSupplier);
}
if (isFinancedDevice()) {
return mContext.getString(
R.string.quick_settings_financed_disclosure_named_management,
organizationName);
} else {
return mDpm.getResources().getString(
QS_MSG_NAMED_MANAGEMENT,
() -> mContext.getString(
R.string.quick_settings_disclosure_named_management,
organizationName),
organizationName);
}
}
private String getManagedAndPersonalProfileFooterText(boolean hasWorkProfile,
boolean hasCACerts, boolean hasCACertsInWorkProfile, boolean isNetworkLoggingEnabled,
String vpnName, String vpnNameWorkProfile, CharSequence workProfileOrganizationName,
boolean isProfileOwnerOfOrganizationOwnedDevice, boolean isWorkProfileOn) {
if (hasCACerts || (hasCACertsInWorkProfile && isWorkProfileOn)) {
return getMonitoringText(
hasCACerts, hasCACertsInWorkProfile, workProfileOrganizationName,
isWorkProfileOn);
}
if (vpnName != null || (vpnNameWorkProfile != null && isWorkProfileOn)) {
return getVpnText(hasWorkProfile, vpnName, vpnNameWorkProfile, isWorkProfileOn);
}
if (hasWorkProfile && isNetworkLoggingEnabled && isWorkProfileOn) {
return getManagedProfileNetworkActivityText();
}
if (isProfileOwnerOfOrganizationOwnedDevice) {
return getMangedDeviceGeneralText(workProfileOrganizationName);
}
return null;
}
private String getMonitoringText(boolean hasCACerts, boolean hasCACertsInWorkProfile,
CharSequence workProfileOrganizationName, boolean isWorkProfileOn) {
if (hasCACertsInWorkProfile && isWorkProfileOn) {
if (workProfileOrganizationName == null) {
return mDpm.getResources().getString(
QS_MSG_WORK_PROFILE_MONITORING, mWorkProfileMonitoringStringSupplier);
}
return mDpm.getResources().getString(
QS_MSG_NAMED_WORK_PROFILE_MONITORING,
() -> mContext.getString(
R.string.quick_settings_disclosure_named_managed_profile_monitoring,
workProfileOrganizationName),
workProfileOrganizationName);
}
if (hasCACerts) {
return mContext.getString(R.string.quick_settings_disclosure_monitoring);
}
return null;
}
private String getVpnText(boolean hasWorkProfile, String vpnName, String vpnNameWorkProfile,
boolean isWorkProfileOn) {
if (vpnName != null && vpnNameWorkProfile != null) {
return mContext.getString(R.string.quick_settings_disclosure_vpns);
}
if (vpnNameWorkProfile != null && isWorkProfileOn) {
return mDpm.getResources().getString(
QS_MSG_WORK_PROFILE_NAMED_VPN,
() -> mContext.getString(
R.string.quick_settings_disclosure_managed_profile_named_vpn,
vpnNameWorkProfile),
vpnNameWorkProfile);
}
if (vpnName != null) {
if (hasWorkProfile) {
return mDpm.getResources().getString(
QS_MSG_PERSONAL_PROFILE_NAMED_VPN,
() -> mContext.getString(
R.string.quick_settings_disclosure_personal_profile_named_vpn,
vpnName),
vpnName);
}
return mContext.getString(R.string.quick_settings_disclosure_named_vpn,
vpnName);
}
return null;
}
private String getManagedProfileNetworkActivityText() {
return mDpm.getResources().getString(
QS_MSG_WORK_PROFILE_NETWORK, mWorkProfileNetworkStringSupplier);
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_NEGATIVE) {
final Intent intent = new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS);
dialog.dismiss();
// This dismisses the shade on opening the activity
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
}
}
private void createDialog() {
mShouldUseSettingsButton.set(false);
mHandler.post(() -> {
String settingsButtonText = getSettingsButton();
final View view = createDialogView();
mMainHandler.post(() -> {
mDialog = new SystemUIDialog(mContext, 0); // Use mContext theme
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setButton(DialogInterface.BUTTON_POSITIVE, getPositiveButton(), this);
mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, mShouldUseSettingsButton.get()
? settingsButtonText : getNegativeButton(), this);
mDialog.setView(view);
if (mView.isAggregatedVisible()) {
mDialogLaunchAnimator.showFromView(mDialog, mView, new DialogCuj(
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG));
} else {
mDialog.show();
}
});
});
}
@VisibleForTesting
Dialog getDialog() {
return mDialog;
}
@VisibleForTesting
View createDialogView() {
if (mSecurityController.isParentalControlsEnabled()) {
return createParentalControlsDialogView();
}
return createOrganizationDialogView();
}
private View createOrganizationDialogView() {
final boolean isDeviceManaged = mSecurityController.isDeviceManaged();
final boolean hasWorkProfile = mSecurityController.hasWorkProfile();
final CharSequence deviceOwnerOrganization =
mSecurityController.getDeviceOwnerOrganizationName();
final boolean hasCACerts = mSecurityController.hasCACertInCurrentUser();
final boolean hasCACertsInWorkProfile = mSecurityController.hasCACertInWorkProfile();
final boolean isNetworkLoggingEnabled = mSecurityController.isNetworkLoggingEnabled();
final String vpnName = mSecurityController.getPrimaryVpnName();
final String vpnNameWorkProfile = mSecurityController.getWorkProfileVpnName();
View dialogView = LayoutInflater.from(mContext)
.inflate(R.layout.quick_settings_footer_dialog, null, false);
// device management section
TextView deviceManagementSubtitle =
dialogView.findViewById(R.id.device_management_subtitle);
deviceManagementSubtitle.setText(getManagementTitle(deviceOwnerOrganization));
CharSequence managementMessage = getManagementMessage(isDeviceManaged,
deviceOwnerOrganization);
if (managementMessage == null) {
dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.GONE);
} else {
dialogView.findViewById(R.id.device_management_disclosures).setVisibility(View.VISIBLE);
TextView deviceManagementWarning =
(TextView) dialogView.findViewById(R.id.device_management_warning);
deviceManagementWarning.setText(managementMessage);
mShouldUseSettingsButton.set(true);
}
// ca certificate section
CharSequence caCertsMessage = getCaCertsMessage(isDeviceManaged, hasCACerts,
hasCACertsInWorkProfile);
if (caCertsMessage == null) {
dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.GONE);
} else {
dialogView.findViewById(R.id.ca_certs_disclosures).setVisibility(View.VISIBLE);
TextView caCertsWarning = (TextView) dialogView.findViewById(R.id.ca_certs_warning);
caCertsWarning.setText(caCertsMessage);
// Make "Open trusted credentials"-link clickable
caCertsWarning.setMovementMethod(new LinkMovementMethod());
TextView caCertsSubtitle = (TextView) dialogView.findViewById(R.id.ca_certs_subtitle);
String caCertsSubtitleMessage = mDpm.getResources().getString(
QS_DIALOG_MONITORING_CA_CERT_SUBTITLE, mMonitoringSubtitleCaCertStringSupplier);
caCertsSubtitle.setText(caCertsSubtitleMessage);
}
// network logging section
CharSequence networkLoggingMessage = getNetworkLoggingMessage(isDeviceManaged,
isNetworkLoggingEnabled);
if (networkLoggingMessage == null) {
dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.GONE);
} else {
dialogView.findViewById(R.id.network_logging_disclosures).setVisibility(View.VISIBLE);
TextView networkLoggingWarning =
(TextView) dialogView.findViewById(R.id.network_logging_warning);
networkLoggingWarning.setText(networkLoggingMessage);
TextView networkLoggingSubtitle = (TextView) dialogView.findViewById(
R.id.network_logging_subtitle);
String networkLoggingSubtitleMessage = mDpm.getResources().getString(
QS_DIALOG_MONITORING_NETWORK_SUBTITLE,
mMonitoringSubtitleNetworkStringSupplier);
networkLoggingSubtitle.setText(networkLoggingSubtitleMessage);
}
// vpn section
CharSequence vpnMessage = getVpnMessage(isDeviceManaged, hasWorkProfile, vpnName,
vpnNameWorkProfile);
if (vpnMessage == null) {
dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.GONE);
} else {
dialogView.findViewById(R.id.vpn_disclosures).setVisibility(View.VISIBLE);
TextView vpnWarning = (TextView) dialogView.findViewById(R.id.vpn_warning);
vpnWarning.setText(vpnMessage);
// Make "Open VPN Settings"-link clickable
vpnWarning.setMovementMethod(new LinkMovementMethod());
TextView vpnSubtitle = (TextView) dialogView.findViewById(R.id.vpn_subtitle);
String vpnSubtitleMessage = mDpm.getResources().getString(
QS_DIALOG_MONITORING_VPN_SUBTITLE, mMonitoringSubtitleVpnStringSupplier);
vpnSubtitle.setText(vpnSubtitleMessage);
}
// Note: if a new section is added, should update configSubtitleVisibility to include
// the handling of the subtitle
configSubtitleVisibility(managementMessage != null,
caCertsMessage != null,
networkLoggingMessage != null,
vpnMessage != null,
dialogView);
return dialogView;
}
private View createParentalControlsDialogView() {
View dialogView = LayoutInflater.from(mContext)
.inflate(R.layout.quick_settings_footer_dialog_parental_controls, null, false);
DeviceAdminInfo info = mSecurityController.getDeviceAdminInfo();
Drawable icon = mSecurityController.getIcon(info);
if (icon != null) {
ImageView imageView = (ImageView) dialogView.findViewById(R.id.parental_controls_icon);
imageView.setImageDrawable(icon);
}
TextView parentalControlsTitle =
(TextView) dialogView.findViewById(R.id.parental_controls_title);
parentalControlsTitle.setText(mSecurityController.getLabel(info));
return dialogView;
}
protected void configSubtitleVisibility(boolean showDeviceManagement, boolean showCaCerts,
boolean showNetworkLogging, boolean showVpn, View dialogView) {
// Device Management title should always been shown
// When there is a Device Management message, all subtitles should be shown
if (showDeviceManagement) {
return;
}
// Hide the subtitle if there is only 1 message shown
int mSectionCountExcludingDeviceMgt = 0;
if (showCaCerts) { mSectionCountExcludingDeviceMgt++; }
if (showNetworkLogging) { mSectionCountExcludingDeviceMgt++; }
if (showVpn) { mSectionCountExcludingDeviceMgt++; }
// No work needed if there is no sections or more than 1 section
if (mSectionCountExcludingDeviceMgt != 1) {
return;
}
if (showCaCerts) {
dialogView.findViewById(R.id.ca_certs_subtitle).setVisibility(View.GONE);
}
if (showNetworkLogging) {
dialogView.findViewById(R.id.network_logging_subtitle).setVisibility(View.GONE);
}
if (showVpn) {
dialogView.findViewById(R.id.vpn_subtitle).setVisibility(View.GONE);
}
}
// This should not be called on the main thread to avoid making an IPC.
@VisibleForTesting
String getSettingsButton() {
return mDpm.getResources().getString(
QS_DIALOG_VIEW_POLICIES, mViewPoliciesButtonStringSupplier);
}
private String getPositiveButton() {
return mContext.getString(R.string.ok);
}
@Nullable
private String getNegativeButton() {
if (mSecurityController.isParentalControlsEnabled()) {
return mContext.getString(R.string.monitoring_button_view_controls);
}
return null;
}
@Nullable
protected CharSequence getManagementMessage(boolean isDeviceManaged,
CharSequence organizationName) {
if (!isDeviceManaged) {
return null;
}
if (organizationName != null) {
if (isFinancedDevice()) {
return mContext.getString(R.string.monitoring_financed_description_named_management,
organizationName, organizationName);
} else {
return mDpm.getResources().getString(
QS_DIALOG_NAMED_MANAGEMENT,
() -> mContext.getString(
R.string.monitoring_description_named_management,
organizationName),
organizationName);
}
}
return mDpm.getResources().getString(QS_DIALOG_MANAGEMENT, mManagementDialogStringSupplier);
}
@Nullable
protected CharSequence getCaCertsMessage(boolean isDeviceManaged, boolean hasCACerts,
boolean hasCACertsInWorkProfile) {
if (!(hasCACerts || hasCACertsInWorkProfile)) return null;
if (isDeviceManaged) {
return mDpm.getResources().getString(
QS_DIALOG_MANAGEMENT_CA_CERT, mManagementDialogCaCertStringSupplier);
}
if (hasCACertsInWorkProfile) {
return mDpm.getResources().getString(
QS_DIALOG_WORK_PROFILE_CA_CERT, mWorkProfileDialogCaCertStringSupplier);
}
return mContext.getString(R.string.monitoring_description_ca_certificate);
}
@Nullable
protected CharSequence getNetworkLoggingMessage(boolean isDeviceManaged,
boolean isNetworkLoggingEnabled) {
if (!isNetworkLoggingEnabled) return null;
if (isDeviceManaged) {
return mDpm.getResources().getString(
QS_DIALOG_MANAGEMENT_NETWORK, mManagementDialogNetworkStringSupplier);
} else {
return mDpm.getResources().getString(
QS_DIALOG_WORK_PROFILE_NETWORK, mWorkProfileDialogNetworkStringSupplier);
}
}
@Nullable
protected CharSequence getVpnMessage(boolean isDeviceManaged, boolean hasWorkProfile,
String vpnName, String vpnNameWorkProfile) {
if (vpnName == null && vpnNameWorkProfile == null) return null;
final SpannableStringBuilder message = new SpannableStringBuilder();
if (isDeviceManaged) {
if (vpnName != null && vpnNameWorkProfile != null) {
String namedVpns = mDpm.getResources().getString(
QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN,
() -> mContext.getString(
R.string.monitoring_description_two_named_vpns,
vpnName, vpnNameWorkProfile),
vpnName, vpnNameWorkProfile);
message.append(namedVpns);
} else {
String name = vpnName != null ? vpnName : vpnNameWorkProfile;
String namedVp = mDpm.getResources().getString(
QS_DIALOG_MANAGEMENT_NAMED_VPN,
() -> mContext.getString(R.string.monitoring_description_named_vpn, name),
name);
message.append(namedVp);
}
} else {
if (vpnName != null && vpnNameWorkProfile != null) {
String namedVpns = mDpm.getResources().getString(
QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN,
() -> mContext.getString(
R.string.monitoring_description_two_named_vpns,
vpnName, vpnNameWorkProfile),
vpnName, vpnNameWorkProfile);
message.append(namedVpns);
} else if (vpnNameWorkProfile != null) {
String namedVpn = mDpm.getResources().getString(
QS_DIALOG_WORK_PROFILE_NAMED_VPN,
() -> mContext.getString(
R.string.monitoring_description_managed_profile_named_vpn,
vpnNameWorkProfile),
vpnNameWorkProfile);
message.append(namedVpn);
} else if (hasWorkProfile) {
String namedVpn = mDpm.getResources().getString(
QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN,
() -> mContext.getString(
R.string.monitoring_description_personal_profile_named_vpn,
vpnName),
vpnName);
message.append(namedVpn);
} else {
message.append(mContext.getString(R.string.monitoring_description_named_vpn,
vpnName));
}
}
message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator));
message.append(mContext.getString(R.string.monitoring_description_vpn_settings),
new VpnSpan(), 0);
return message;
}
@VisibleForTesting
CharSequence getManagementTitle(CharSequence deviceOwnerOrganization) {
if (deviceOwnerOrganization != null && isFinancedDevice()) {
return mContext.getString(R.string.monitoring_title_financed_device,
deviceOwnerOrganization);
} else {
return mDpm.getResources().getString(
QS_DIALOG_MANAGEMENT_TITLE,
mManagementTitleSupplier);
}
}
private boolean isFinancedDevice() {
return mSecurityController.isDeviceManaged()
&& mSecurityController.getDeviceOwnerType(
mSecurityController.getDeviceOwnerComponentOnAnyUser())
== DEVICE_OWNER_TYPE_FINANCED;
}
private final Runnable mUpdatePrimaryIcon = new Runnable() {
@Override
public void run() {
if (mPrimaryFooterIconDrawable != null) {
mPrimaryFooterIcon.setImageDrawable(mPrimaryFooterIconDrawable);
} else {
mPrimaryFooterIcon.setImageResource(mFooterIconId);
if (mFooterIcon instanceof Icon.Loaded) {
mPrimaryFooterIcon.setImageDrawable(((Icon.Loaded) mFooterIcon).getDrawable());
} else if (mFooterIcon instanceof Icon.Resource) {
mPrimaryFooterIcon.setImageResource(((Icon.Resource) mFooterIcon).getRes());
}
}
};
@@ -890,10 +210,18 @@ class QSSecurityFooter extends ViewController<View>
if (mFooterTextContent != null) {
mFooterText.setText(mFooterTextContent);
}
mView.setVisibility(mIsVisible || DEBUG_FORCE_VISIBLE ? View.VISIBLE : View.GONE);
mView.setVisibility(mIsVisible ? View.VISIBLE : View.GONE);
if (mVisibilityChangedListener != null) {
mVisibilityChangedListener.onVisibilityChanged(mView.getVisibility());
}
if (mIsVisible && mIsClickable) {
mView.setClickable(true);
mView.findViewById(R.id.footer_icon).setVisibility(View.VISIBLE);
} else {
mView.setClickable(false);
mView.findViewById(R.id.footer_icon).setVisibility(View.GONE);
}
}
};
@@ -929,25 +257,4 @@ class QSSecurityFooter extends ViewController<View>
}
}
}
protected class VpnSpan extends ClickableSpan {
@Override
public void onClick(View widget) {
final Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS);
mDialog.dismiss();
// This dismisses the shade on opening the activity
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
}
// for testing, to compare two CharSequences containing VpnSpans
@Override
public boolean equals(Object object) {
return object instanceof VpnSpan;
}
@Override
public int hashCode() {
return 314159257; // prime
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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,
)
}
}

View File

@@ -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,
)

View File

@@ -0,0 +1,321 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.qs.footer.ui.binder
import android.content.Context
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.common.ui.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)
}
}
}

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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)?,
)

View File

@@ -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"
}
}

View File

@@ -44,6 +44,7 @@ import com.android.settingslib.Utils
import com.android.systemui.FontSizeUtils
import com.android.systemui.R
import com.android.systemui.animation.LaunchableView
import com.android.systemui.animation.LaunchableViewDelegate
import com.android.systemui.plugins.qs.QSIconView
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTile.BooleanState
@@ -138,8 +139,11 @@ open class QSTileViewImpl @JvmOverloads constructor(
private var lastStateDescription: CharSequence? = null
private var tileState = false
private var lastState = INVALID
private var blockVisibilityChanges = false
private var lastVisibility = View.VISIBLE
private val launchableViewDelegate = LaunchableViewDelegate(
this,
superSetVisibility = { super.setVisibility(it) },
superSetTransitionVisibility = { super.setTransitionVisibility(it) },
)
private val locInScreen = IntArray(2)
@@ -343,33 +347,15 @@ open class QSTileViewImpl @JvmOverloads constructor(
}
override fun setShouldBlockVisibilityChanges(block: Boolean) {
blockVisibilityChanges = block
if (block) {
lastVisibility = visibility
} else {
visibility = lastVisibility
}
launchableViewDelegate.setShouldBlockVisibilityChanges(block)
}
override fun setVisibility(visibility: Int) {
if (blockVisibilityChanges) {
lastVisibility = visibility
return
}
super.setVisibility(visibility)
launchableViewDelegate.setVisibility(visibility)
}
override fun setTransitionVisibility(visibility: Int) {
if (blockVisibilityChanges) {
// View.setTransitionVisibility just sets the visibility flag, so we don't have to save
// the transition visibility separately from the normal visibility.
lastVisibility = visibility
return
}
super.setTransitionVisibility(visibility)
launchableViewDelegate.setTransitionVisibility(visibility)
}
// Accessibility

View File

@@ -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),
)
}
}
}

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -20,12 +20,28 @@ import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import com.android.systemui.animation.LaunchableView;
import com.android.systemui.animation.LaunchableViewDelegate;
import kotlin.Unit;
/**
* A frame layout which does not have overlapping renderings commands and therefore does not need a
* layer when alpha is changed.
*/
public class AlphaOptimizedFrameLayout extends FrameLayout
public class AlphaOptimizedFrameLayout extends FrameLayout implements LaunchableView
{
private final LaunchableViewDelegate mLaunchableViewDelegate = new LaunchableViewDelegate(
this,
visibility -> {
super.setVisibility(visibility);
return Unit.INSTANCE;
},
visibility -> {
super.setTransitionVisibility(visibility);
return Unit.INSTANCE;
});
public AlphaOptimizedFrameLayout(Context context) {
super(context);
}
@@ -47,4 +63,19 @@ public class AlphaOptimizedFrameLayout extends FrameLayout
public boolean hasOverlappingRendering() {
return false;
}
@Override
public void setShouldBlockVisibilityChanges(boolean block) {
mLaunchableViewDelegate.setShouldBlockVisibilityChanges(block);
}
@Override
public void setVisibility(int visibility) {
mLaunchableViewDelegate.setVisibility(visibility);
}
@Override
public void setTransitionVisibility(int visibility) {
mLaunchableViewDelegate.setTransitionVisibility(visibility);
}
}

View File

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

View File

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

View File

@@ -10,12 +10,10 @@ import android.graphics.Rect
import android.os.Looper
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.util.Log
import android.view.IRemoteAnimationFinishedCallback
import android.view.RemoteAnimationAdapter
import android.view.RemoteAnimationTarget
import android.view.SurfaceControl
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
@@ -51,7 +49,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
@Mock lateinit var listener: ActivityLaunchAnimator.Listener
@Spy private val controller = TestLaunchAnimatorController(launchContainer)
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
@Mock lateinit var failHandler: Log.TerribleFailureHandler
private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
@get:Rule val rule = MockitoJUnit.rule()
@@ -187,13 +184,6 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
verify(controller).onLaunchAnimationStart(anyBoolean())
}
@Test
fun controllerFromOrphanViewReturnsNullAndIsATerribleFailure() {
Log.setWtfHandler(failHandler)
assertNull(ActivityLaunchAnimator.Controller.fromView(View(mContext)))
verify(failHandler).onTerribleFailure(any(), any(), anyBoolean())
}
private fun fakeWindow(): RemoteAnimationTarget {
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
val taskInfo = ActivityManager.RunningTaskInfo()

View File

@@ -18,6 +18,7 @@ package com.android.systemui.broadcast
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
import android.os.Looper
@@ -32,22 +33,27 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import junit.framework.Assert.assertSame
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.util.concurrent.Executor
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
@@ -381,6 +387,39 @@ class BroadcastDispatcherTest : SysuiTestCase() {
.clearPendingRemoval(broadcastReceiver, user1.identifier)
}
@Test
fun testBroadcastFlow() = runBlockingTest {
val flow = broadcastDispatcher.broadcastFlow(intentFilter, user1) { intent, receiver ->
intent to receiver
}
// Collect the values into collectedValues.
val collectedValues = mutableListOf<Pair<Intent, BroadcastReceiver>>()
val job = launch {
flow.collect { collectedValues.add(it) }
}
testableLooper.processAllMessages()
verify(mockUBRUser1).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))
val receiver = argumentCaptor.value.receiver
// Simulate fake broadcasted intents.
val fakeIntents = listOf<Intent>(mock(), mock(), mock())
fakeIntents.forEach { receiver.onReceive(mockContext, it) }
// The intents should have been collected.
advanceUntilIdle()
val expectedValues = fakeIntents.map { it to receiver }
assertThat(collectedValues).containsExactlyElementsIn(expectedValues)
// Stop the collection.
job.cancel()
testableLooper.processAllMessages()
verify(mockUBRUser1).unregisterReceiver(receiver)
}
private fun setUserMock(mockContext: Context, user: UserHandle) {
`when`(mockContext.user).thenReturn(user)
`when`(mockContext.userId).thenReturn(user.identifier)

View File

@@ -16,6 +16,8 @@
package com.android.systemui.qs;
import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
@@ -34,6 +36,7 @@ import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -185,9 +188,9 @@ public class FgsManagerControllerTest extends SysuiTestCase {
public void testChangesSinceLastDialog() throws RemoteException {
setUserProfiles(0);
Assert.assertFalse(mFmc.getChangesSinceDialog());
Assert.assertFalse(mFmc.getNewChangesSinceDialogWasDismissed());
mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg", 0, true);
Assert.assertTrue(mFmc.getChangesSinceDialog());
Assert.assertTrue(mFmc.getNewChangesSinceDialogWasDismissed());
}
@Test
@@ -222,7 +225,41 @@ public class FgsManagerControllerTest extends SysuiTestCase {
Assert.assertEquals(2, mFmc.getNumRunningPackages());
}
@Test
public void testButtonVisibilityOnShowAllowlistButtonFlagChange() throws Exception {
setUserProfiles(0);
setBackgroundRestrictionExemptionReason("pkg", 12345, REASON_ALLOWLISTED_PACKAGE);
final Binder binder = new Binder();
setShowStopButtonForUserAllowlistedApps(true);
mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true);
Assert.assertEquals(1, mFmc.visibleButtonsCount());
mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, false);
Assert.assertEquals(0, mFmc.visibleButtonsCount());
setShowStopButtonForUserAllowlistedApps(false);
mIForegroundServiceObserver.onForegroundStateChanged(binder, "pkg", 0, true);
Assert.assertEquals(0, mFmc.visibleButtonsCount());
}
private void setShowStopButtonForUserAllowlistedApps(boolean enable) {
mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
enable ? "true" : "false", false);
mBackgroundExecutor.advanceClockToLast();
mBackgroundExecutor.runAllReady();
}
private void setBackgroundRestrictionExemptionReason(String pkgName, int uid, int reason)
throws Exception {
Mockito.doReturn(uid)
.when(mPackageManager)
.getPackageUidAsUser(pkgName, UserHandle.getUserId(uid));
Mockito.doReturn(reason)
.when(mIActivityManager)
.getBackgroundRestrictionExemptionReason(uid);
}
FgsManagerController createFgsManagerController() throws RemoteException {
ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor =
@@ -232,7 +269,7 @@ public class FgsManagerControllerTest extends SysuiTestCase {
ArgumentCaptor<BroadcastReceiver> showFgsManagerReceiverArgumentCaptor =
ArgumentCaptor.forClass(BroadcastReceiver.class);
FgsManagerController result = new FgsManagerController(
FgsManagerController result = new FgsManagerControllerImpl(
mContext,
mMainExecutor,
mBackgroundExecutor,

View File

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

View File

@@ -100,6 +100,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
private TextView mFooterText;
private TestableImageView mPrimaryFooterIcon;
private QSSecurityFooter mFooter;
private QSSecurityFooterUtils mFooterUtils;
@Mock
private SecurityController mSecurityController;
@Mock
@@ -118,13 +119,16 @@ public class QSSecurityFooterTest extends SysuiTestCase {
MockitoAnnotations.initMocks(this);
mTestableLooper = TestableLooper.get(this);
Looper looper = mTestableLooper.getLooper();
Handler mainHandler = new Handler(looper);
when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class));
mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext)
.replace("ImageView", TestableImageView.class)
.build().inflate(R.layout.quick_settings_security_footer, null, false);
mFooter = new QSSecurityFooter(mRootView, mUserTracker, new Handler(looper),
mActivityStarter, mSecurityController, mDialogLaunchAnimator, looper,
mBroadcastDispatcher);
mFooterUtils = new QSSecurityFooterUtils(getContext(),
getContext().getSystemService(DevicePolicyManager.class), mUserTracker,
mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator);
mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper,
mBroadcastDispatcher, mFooterUtils);
mFooterText = mRootView.findViewById(R.id.footer_text);
mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon);
@@ -520,7 +524,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
when(mSecurityController.isDeviceManaged()).thenReturn(true);
assertEquals(mContext.getString(R.string.monitoring_title_device_owned),
mFooter.getManagementTitle(MANAGING_ORGANIZATION));
mFooterUtils.getManagementTitle(MANAGING_ORGANIZATION));
}
@Test
@@ -531,12 +535,12 @@ public class QSSecurityFooterTest extends SysuiTestCase {
assertEquals(mContext.getString(R.string.monitoring_title_financed_device,
MANAGING_ORGANIZATION),
mFooter.getManagementTitle(MANAGING_ORGANIZATION));
mFooterUtils.getManagementTitle(MANAGING_ORGANIZATION));
}
@Test
public void testGetManagementMessage_noManagement() {
assertEquals(null, mFooter.getManagementMessage(
assertEquals(null, mFooterUtils.getManagementMessage(
/* isDeviceManaged= */ false, MANAGING_ORGANIZATION));
}
@@ -544,10 +548,10 @@ public class QSSecurityFooterTest extends SysuiTestCase {
public void testGetManagementMessage_deviceOwner() {
assertEquals(mContext.getString(R.string.monitoring_description_named_management,
MANAGING_ORGANIZATION),
mFooter.getManagementMessage(
mFooterUtils.getManagementMessage(
/* isDeviceManaged= */ true, MANAGING_ORGANIZATION));
assertEquals(mContext.getString(R.string.monitoring_description_management),
mFooter.getManagementMessage(
mFooterUtils.getManagementMessage(
/* isDeviceManaged= */ true,
/* organizationName= */ null));
}
@@ -560,68 +564,68 @@ public class QSSecurityFooterTest extends SysuiTestCase {
assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management,
MANAGING_ORGANIZATION, MANAGING_ORGANIZATION),
mFooter.getManagementMessage(
mFooterUtils.getManagementMessage(
/* isDeviceManaged= */ true, MANAGING_ORGANIZATION));
}
@Test
public void testGetCaCertsMessage() {
assertEquals(null, mFooter.getCaCertsMessage(true, false, false));
assertEquals(null, mFooter.getCaCertsMessage(false, false, false));
assertEquals(null, mFooterUtils.getCaCertsMessage(true, false, false));
assertEquals(null, mFooterUtils.getCaCertsMessage(false, false, false));
assertEquals(mContext.getString(R.string.monitoring_description_management_ca_certificate),
mFooter.getCaCertsMessage(true, true, true));
mFooterUtils.getCaCertsMessage(true, true, true));
assertEquals(mContext.getString(R.string.monitoring_description_management_ca_certificate),
mFooter.getCaCertsMessage(true, false, true));
mFooterUtils.getCaCertsMessage(true, false, true));
assertEquals(mContext.getString(
R.string.monitoring_description_managed_profile_ca_certificate),
mFooter.getCaCertsMessage(false, false, true));
mFooterUtils.getCaCertsMessage(false, false, true));
assertEquals(mContext.getString(
R.string.monitoring_description_ca_certificate),
mFooter.getCaCertsMessage(false, true, false));
mFooterUtils.getCaCertsMessage(false, true, false));
}
@Test
public void testGetNetworkLoggingMessage() {
// Test network logging message on a device with a device owner.
// Network traffic may be monitored on the device.
assertEquals(null, mFooter.getNetworkLoggingMessage(true, false));
assertEquals(null, mFooterUtils.getNetworkLoggingMessage(true, false));
assertEquals(mContext.getString(R.string.monitoring_description_management_network_logging),
mFooter.getNetworkLoggingMessage(true, true));
mFooterUtils.getNetworkLoggingMessage(true, true));
// Test network logging message on a device with a managed profile owner
// Network traffic may be monitored on the work profile.
assertEquals(null, mFooter.getNetworkLoggingMessage(false, false));
assertEquals(null, mFooterUtils.getNetworkLoggingMessage(false, false));
assertEquals(
mContext.getString(R.string.monitoring_description_managed_profile_network_logging),
mFooter.getNetworkLoggingMessage(false, true));
mFooterUtils.getNetworkLoggingMessage(false, true));
}
@Test
public void testGetVpnMessage() {
assertEquals(null, mFooter.getVpnMessage(true, true, null, null));
assertEquals(null, mFooterUtils.getVpnMessage(true, true, null, null));
assertEquals(addLink(mContext.getString(R.string.monitoring_description_two_named_vpns,
VPN_PACKAGE, VPN_PACKAGE_2)),
mFooter.getVpnMessage(true, true, VPN_PACKAGE, VPN_PACKAGE_2));
mFooterUtils.getVpnMessage(true, true, VPN_PACKAGE, VPN_PACKAGE_2));
assertEquals(addLink(mContext.getString(R.string.monitoring_description_two_named_vpns,
VPN_PACKAGE, VPN_PACKAGE_2)),
mFooter.getVpnMessage(false, true, VPN_PACKAGE, VPN_PACKAGE_2));
mFooterUtils.getVpnMessage(false, true, VPN_PACKAGE, VPN_PACKAGE_2));
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
VPN_PACKAGE)),
mFooter.getVpnMessage(true, false, VPN_PACKAGE, null));
mFooterUtils.getVpnMessage(true, false, VPN_PACKAGE, null));
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
VPN_PACKAGE)),
mFooter.getVpnMessage(false, false, VPN_PACKAGE, null));
mFooterUtils.getVpnMessage(false, false, VPN_PACKAGE, null));
assertEquals(addLink(mContext.getString(R.string.monitoring_description_named_vpn,
VPN_PACKAGE_2)),
mFooter.getVpnMessage(true, true, null, VPN_PACKAGE_2));
mFooterUtils.getVpnMessage(true, true, null, VPN_PACKAGE_2));
assertEquals(addLink(mContext.getString(
R.string.monitoring_description_managed_profile_named_vpn,
VPN_PACKAGE_2)),
mFooter.getVpnMessage(false, true, null, VPN_PACKAGE_2));
mFooterUtils.getVpnMessage(false, true, null, VPN_PACKAGE_2));
assertEquals(addLink(mContext.getString(
R.string.monitoring_description_personal_profile_named_vpn,
VPN_PACKAGE)),
mFooter.getVpnMessage(false, true, VPN_PACKAGE, null));
mFooterUtils.getVpnMessage(false, true, VPN_PACKAGE, null));
}
@Test
@@ -631,19 +635,19 @@ public class QSSecurityFooterTest extends SysuiTestCase {
// Device Management subtitle should be shown when there is Device Management section only
// Other sections visibility will be set somewhere else so it will not be tested here
mFooter.configSubtitleVisibility(true, false, false, false, view);
mFooterUtils.configSubtitleVisibility(true, false, false, false, view);
assertEquals(View.VISIBLE,
view.findViewById(R.id.device_management_subtitle).getVisibility());
// If there are multiple sections, all subtitles should be shown
mFooter.configSubtitleVisibility(true, true, false, false, view);
mFooterUtils.configSubtitleVisibility(true, true, false, false, view);
assertEquals(View.VISIBLE,
view.findViewById(R.id.device_management_subtitle).getVisibility());
assertEquals(View.VISIBLE,
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
// If there are multiple sections, all subtitles should be shown
mFooter.configSubtitleVisibility(true, true, true, true, view);
mFooterUtils.configSubtitleVisibility(true, true, true, true, view);
assertEquals(View.VISIBLE,
view.findViewById(R.id.device_management_subtitle).getVisibility());
assertEquals(View.VISIBLE,
@@ -655,7 +659,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
// If there are multiple sections, all subtitles should be shown, event if there is no
// Device Management section
mFooter.configSubtitleVisibility(false, true, true, true, view);
mFooterUtils.configSubtitleVisibility(false, true, true, true, view);
assertEquals(View.VISIBLE,
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
assertEquals(View.VISIBLE,
@@ -664,13 +668,13 @@ public class QSSecurityFooterTest extends SysuiTestCase {
view.findViewById(R.id.vpn_subtitle).getVisibility());
// If there is only 1 section, the title should be hidden
mFooter.configSubtitleVisibility(false, true, false, false, view);
mFooterUtils.configSubtitleVisibility(false, true, false, false, view);
assertEquals(View.GONE,
view.findViewById(R.id.ca_certs_subtitle).getVisibility());
mFooter.configSubtitleVisibility(false, false, true, false, view);
mFooterUtils.configSubtitleVisibility(false, false, true, false, view);
assertEquals(View.GONE,
view.findViewById(R.id.network_logging_subtitle).getVisibility());
mFooter.configSubtitleVisibility(false, false, false, true, view);
mFooterUtils.configSubtitleVisibility(false, false, false, true, view);
assertEquals(View.GONE,
view.findViewById(R.id.vpn_subtitle).getVisibility());
}
@@ -690,6 +694,9 @@ public class QSSecurityFooterTest extends SysuiTestCase {
@Test
public void testParentalControls() {
// Make sure the security footer is visible, so that the images are updated.
when(mSecurityController.isProfileOwnerOfOrganizationOwnedDevice()).thenReturn(true);
when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
Drawable testDrawable = new VectorDrawable();
@@ -719,7 +726,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
when(mSecurityController.isParentalControlsEnabled()).thenReturn(true);
when(mSecurityController.getLabel(any())).thenReturn(PARENTAL_CONTROLS_LABEL);
View view = mFooter.createDialogView();
View view = mFooterUtils.createDialogView();
TextView textView = (TextView) view.findViewById(R.id.parental_controls_title);
assertEquals(PARENTAL_CONTROLS_LABEL, textView.getText());
}
@@ -742,7 +749,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
.thenReturn(DEVICE_OWNER_TYPE_FINANCED);
View view = mFooter.createDialogView();
View view = mFooterUtils.createDialogView();
TextView managementSubtitle = view.findViewById(R.id.device_management_subtitle);
assertEquals(View.VISIBLE, managementSubtitle.getVisibility());
@@ -753,7 +760,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
assertEquals(mContext.getString(R.string.monitoring_financed_description_named_management,
MANAGING_ORGANIZATION, MANAGING_ORGANIZATION), managementMessage.getText());
assertEquals(mContext.getString(R.string.monitoring_button_view_policies),
mFooter.getSettingsButton());
mFooterUtils.getSettingsButton());
}
@Test
@@ -773,7 +780,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
AlertDialog dialog = dialogCaptor.getValue();
dialog.create();
assertEquals(mFooter.getSettingsButton(),
assertEquals(mFooterUtils.getSettingsButton(),
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getText());
dialog.dismiss();
@@ -816,8 +823,8 @@ public class QSSecurityFooterTest extends SysuiTestCase {
new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG));
mTestableLooper.processAllMessages();
assertTrue(mFooter.getDialog().isShowing());
mFooter.getDialog().dismiss();
assertTrue(mFooterUtils.getDialog().isShowing());
mFooterUtils.getDialog().dismiss();
}
private CharSequence addLink(CharSequence description) {
@@ -825,7 +832,7 @@ public class QSSecurityFooterTest extends SysuiTestCase {
message.append(description);
message.append(mContext.getString(R.string.monitoring_description_vpn_settings_separator));
message.append(mContext.getString(R.string.monitoring_description_vpn_settings),
mFooter.new VpnSpan(), 0);
mFooterUtils.new VpnSpan(), 0);
return message;
}
}

View File

@@ -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,
)
)
}
}

View File

@@ -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()
}
}

View File

@@ -56,7 +56,6 @@ class FakeFeatureFlags : FeatureFlags {
stringFlags.put(flag.id, value)
}
override fun isEnabled(flag: UnreleasedFlag): Boolean = requireBooleanValue(flag.id)
override fun isEnabled(flag: ReleasedFlag): Boolean = requireBooleanValue(flag.id)

View File

@@ -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
}

View File

@@ -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,
)
}
}

View File

@@ -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)
}
}

View File

@@ -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,
)
}

View File

@@ -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 = "",
)
}

View File

@@ -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 }
}
}

View File

@@ -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",
)
}

View File

@@ -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",
)
}