Merge changes from topic "scPermDialog" into udc-qpr-dev am: afb8816434

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23262861

Change-Id: Iea1c064580ded620c14db273e3ab82a8c317e5e7
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Giulio Fiscella
2023-07-24 14:21:48 +00:00
committed by Automerger Merge Worker
29 changed files with 2915 additions and 105 deletions

View File

@@ -32,11 +32,10 @@ import android.view.ViewRootImpl
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowManager import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
import android.widget.FrameLayout
import com.android.app.animation.Interpolators import com.android.app.animation.Interpolators
import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CujType import com.android.internal.jank.InteractionJankMonitor.CujType
import com.android.systemui.animation.view.LaunchableFrameLayout import com.android.systemui.util.maybeForceFullscreen
import com.android.systemui.util.registerAnimationOnBackInvoked import com.android.systemui.util.registerAnimationOnBackInvoked
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -622,96 +621,12 @@ private class AnimatedDialog(
viewGroupWithBackground viewGroupWithBackground
} else { } else {
// We will make the dialog window (and therefore its DecorView) fullscreen to make val (dialogContentWithBackground, decorViewLayoutListener) =
// it possible to animate outside its bounds. dialog.maybeForceFullscreen()!!
// this.decorViewLayoutListener = decorViewLayoutListener
// Before that, we add a new View as a child of the DecorView with the same size and
// gravity as that DecorView, then we add all original children of the DecorView to
// that new View. Finally we remove the background of the DecorView and add it to
// the new View, then we make the DecorView fullscreen. This new View now acts as a
// fake (non fullscreen) window.
//
// On top of that, we also add a fullscreen transparent background between the
// DecorView and the view that we added so that we can dismiss the dialog when this
// view is clicked. This is necessary because DecorView overrides onTouchEvent and
// therefore we can't set the click listener directly on the (now fullscreen)
// DecorView.
val fullscreenTransparentBackground = FrameLayout(dialog.context)
decorView.addView(
fullscreenTransparentBackground,
0 /* index */,
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
)
val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
dialogContentWithBackground.background = decorView.background
// Make the window background transparent. Note that setting the window (or
// DecorView) background drawable to null leads to issues with background color (not
// being transparent) or with insets that are not refreshed. Therefore we need to
// set it to something not null, hence we are using android.R.color.transparent
// here.
window.setBackgroundDrawableResource(android.R.color.transparent)
// Close the dialog when clicking outside of it.
fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
dialogContentWithBackground.isClickable = true
// Make sure the transparent and dialog backgrounds are not focusable by
// accessibility
// features.
fullscreenTransparentBackground.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO
dialogContentWithBackground.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO
fullscreenTransparentBackground.addView(
dialogContentWithBackground,
FrameLayout.LayoutParams(
window.attributes.width,
window.attributes.height,
window.attributes.gravity
)
)
// Move all original children of the DecorView to the new View we just added.
for (i in 1 until decorView.childCount) {
val view = decorView.getChildAt(1)
decorView.removeViewAt(1)
dialogContentWithBackground.addView(view)
}
// Make the window fullscreen and add a layout listener to ensure it stays
// fullscreen.
window.setLayout(MATCH_PARENT, MATCH_PARENT)
decorViewLayoutListener =
View.OnLayoutChangeListener {
v,
left,
top,
right,
bottom,
oldLeft,
oldTop,
oldRight,
oldBottom ->
if (
window.attributes.width != MATCH_PARENT ||
window.attributes.height != MATCH_PARENT
) {
// The dialog size changed, copy its size to dialogContentWithBackground
// and make the dialog window full screen again.
val layoutParams = dialogContentWithBackground.layoutParams
layoutParams.width = window.attributes.width
layoutParams.height = window.attributes.height
dialogContentWithBackground.layoutParams = layoutParams
window.setLayout(MATCH_PARENT, MATCH_PARENT)
}
}
decorView.addOnLayoutChangeListener(decorViewLayoutListener)
dialogContentWithBackground dialogContentWithBackground
} }
this.dialogContentWithBackground = dialogContentWithBackground this.dialogContentWithBackground = dialogContentWithBackground
dialogContentWithBackground.setTag(R.id.tag_dialog_background, true) dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)

View File

@@ -18,6 +18,9 @@ package com.android.systemui.util
import android.app.Dialog import android.app.Dialog
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import android.window.OnBackInvokedDispatcher import android.window.OnBackInvokedDispatcher
import com.android.systemui.animation.back.BackAnimationSpec import com.android.systemui.animation.back.BackAnimationSpec
import com.android.systemui.animation.back.BackTransformation import com.android.systemui.animation.back.BackTransformation
@@ -25,6 +28,7 @@ import com.android.systemui.animation.back.applyTo
import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
import com.android.systemui.animation.back.onBackAnimationCallbackFrom import com.android.systemui.animation.back.onBackAnimationCallbackFrom
import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached
import com.android.systemui.animation.view.LaunchableFrameLayout
/** /**
* Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec]. * Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec].
@@ -49,3 +53,110 @@ fun Dialog.registerAnimationOnBackInvoked(
), ),
) )
} }
/**
* Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate
* outside its bounds. No-op if the dialog is already fullscreen.
*
* <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view
* and a layout listener. The new view matches the original dialog DecorView in size, position, and
* background. This new view will be a child of the modified, transparent, fullscreen DecorView. The
* layout listener is listening to changes to the modified DecorView. It is the responsibility of
* the caller to deregister the listener when the dialog is dismissed.
*/
fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? {
// Create the dialog so that its onCreate() method is called, which usually sets the dialog
// content.
create()
val window = window!!
val decorView = window.decorView as ViewGroup
val isWindowFullscreen =
window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
if (isWindowFullscreen) {
return null
}
// We will make the dialog window (and therefore its DecorView) fullscreen to make it possible
// to animate outside its bounds.
//
// Before that, we add a new View as a child of the DecorView with the same size and gravity as
// that DecorView, then we add all original children of the DecorView to that new View. Finally
// we remove the background of the DecorView and add it to the new View, then we make the
// DecorView fullscreen. This new View now acts as a fake (non fullscreen) window.
//
// On top of that, we also add a fullscreen transparent background between the DecorView and the
// view that we added so that we can dismiss the dialog when this view is clicked. This is
// necessary because DecorView overrides onTouchEvent and therefore we can't set the click
// listener directly on the (now fullscreen) DecorView.
val fullscreenTransparentBackground = FrameLayout(context)
decorView.addView(
fullscreenTransparentBackground,
0 /* index */,
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
)
val dialogContentWithBackground = LaunchableFrameLayout(context)
dialogContentWithBackground.background = decorView.background
// Make the window background transparent. Note that setting the window (or DecorView)
// background drawable to null leads to issues with background color (not being transparent) or
// with insets that are not refreshed. Therefore we need to set it to something not null, hence
// we are using android.R.color.transparent here.
window.setBackgroundDrawableResource(android.R.color.transparent)
// Close the dialog when clicking outside of it.
fullscreenTransparentBackground.setOnClickListener { dismiss() }
dialogContentWithBackground.isClickable = true
// Make sure the transparent and dialog backgrounds are not focusable by accessibility
// features.
fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
fullscreenTransparentBackground.addView(
dialogContentWithBackground,
FrameLayout.LayoutParams(
window.attributes.width,
window.attributes.height,
window.attributes.gravity
)
)
// Move all original children of the DecorView to the new View we just added.
for (i in 1 until decorView.childCount) {
val view = decorView.getChildAt(1)
decorView.removeViewAt(1)
dialogContentWithBackground.addView(view)
}
// Make the window fullscreen and add a layout listener to ensure it stays fullscreen.
window.setLayout(MATCH_PARENT, MATCH_PARENT)
val decorViewLayoutListener =
View.OnLayoutChangeListener {
v,
left,
top,
right,
bottom,
oldLeft,
oldTop,
oldRight,
oldBottom ->
if (
window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT
) {
// The dialog size changed, copy its size to dialogContentWithBackground and make
// the dialog window full screen again.
val layoutParams = dialogContentWithBackground.layoutParams
layoutParams.width = window.attributes.width
layoutParams.height = window.attributes.height
dialogContentWithBackground.layoutParams = layoutParams
window.setLayout(MATCH_PARENT, MATCH_PARENT)
}
}
decorView.addOnLayoutChangeListener(decorViewLayoutListener)
return dialogContentWithBackground to decorViewLayoutListener
}

View File

@@ -0,0 +1,31 @@
<!--
~ Copyright (C) 2023 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.
-->
<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. -->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
<path
android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
android:fillColor="?androidprv:attr/materialColorOnSurface"/>
</vector>

View File

@@ -0,0 +1,31 @@
<!--
~ Copyright (C) 2023 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.
-->
<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. -->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z"
android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
<path
android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527"
android:fillColor="?androidprv:attr/materialColorOnSurface"/>
</vector>

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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:id="@android:id/background">
<shape
android:shape="oval"
android:id="@id/background"
android:gravity="center">
<size
android:height="24dp"
android:width="24dp"/>
<solid android:color="@android:color/white"/>
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
<solid android:color="@android:color/white" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
<solid android:color="@android:color/white" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
<solid android:color="@android:color/white" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
<solid android:color="@android:color/white" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners
android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,25 @@
<!--
~ Copyright (C) 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9.55,18l-5.7,-5.7 1.425,-1.425L9.55,15.15l9.175,-9.175L20.15,7.4z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<!--
~ Copyright (C) 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dp"
android:height="24.0dp"
android:viewportWidth="48.0"
android:viewportHeight="48.0">
<path
android:fillColor="#FF000000"
android:pathData="M26.0,14.0l-4.0,0.0l0.0,4.0l4.0,0.0l0.0,-4.0zm0.0,8.0l-4.0,0.0l0.0,12.0l4.0,0.0L26.0,22.0zm8.0,-19.98L14.0,2.0c-2.21,0.0 -4.0,1.79 -4.0,4.0l0.0,36.0c0.0,2.21 1.79,4.0 4.0,4.0l20.0,0.0c2.21,0.0 4.0,-1.79 4.0,-4.0L38.0,6.0c0.0,-2.21 -1.79,-3.98 -4.0,-3.98zM34.0,38.0L14.0,38.0L14.0,10.0l20.0,0.0l0.0,28.0z"/>
</vector>

View File

@@ -0,0 +1,30 @@
<!--
~ Copyright (C) 2023 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.
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
<path
android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
android:fillColor="?androidprv:attr/materialColorOnSurface"/>
</vector>

View File

@@ -0,0 +1,30 @@
<!--
~ Copyright (C) 2023 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.
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z"
android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
<path
android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527"
android:fillColor="?androidprv:attr/materialColorOnSurface"/>
</vector>

View File

@@ -0,0 +1,26 @@
<!--
~ Copyright (C) 2023 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.
-->
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:maxLines="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
style="@style/Widget.Dialog.Button.BorderButton"/>

View File

@@ -0,0 +1,89 @@
<!--
~ Copyright (C) 2023 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.
-->
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/privacy_dialog_item_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="28dp"
app:cardElevation="0dp"
app:cardBackgroundColor="?androidprv:attr/materialColorSurfaceBright">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/privacy_dialog_item_header"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="20dp"
android:paddingBottom="20dp"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<ImageView
android:id="@+id/privacy_dialog_item_header_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:importantForAccessibility="no" />
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_centerVertical="true">
<TextView
android:id="@+id/privacy_dialog_item_header_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:hyphenationFrequency="normalFast"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Title" />
<TextView
android:id="@+id/privacy_dialog_item_header_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Summary" />
</LinearLayout>
<ImageView
android:id="@+id/privacy_dialog_item_header_expand_toggle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:id="@+id/privacy_dialog_item_header_expanded_layout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:visibility="gone">
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,109 @@
<!--
~ Copyright (C) 2023 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.
-->
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:layout_width="@dimen/large_dialog_width"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="vertical">
<!-- Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:fontFamily="@*android:string/config_headlineFontFamily"
android:text="@string/privacy_dialog_title"
android:layout_marginBottom="12dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/privacy_dialog_summary"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
android:gravity="center"
android:layout_marginBottom="20dp"/>
</LinearLayout>
<!-- Items -->
<LinearLayout
android:id="@+id/privacy_dialog_items_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:orientation="vertical"
/>
<!-- Buttons -->
<LinearLayout
android:id="@+id/button_layout"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="18dp"
android:clickable="false"
android:focusable="false">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="start|center_vertical"
android:orientation="vertical">
<Button
android:id="@+id/privacy_dialog_more_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/privacy_dialog_more_button"
android:ellipsize="end"
android:maxLines="1"
style="@style/Widget.Dialog.Button.BorderButton"
android:clickable="true"
android:focusable="true"
android:gravity="center"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical">
<Button
android:id="@+id/privacy_dialog_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/privacy_dialog_done_button"
android:ellipsize="end"
android:maxLines="1"
style="@style/Widget.Dialog.Button.BorderButton"
android:clickable="true"
android:focusable="true"
android:gravity="center"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -16,7 +16,8 @@
* limitations under the License. * limitations under the License.
*/ */
--> -->
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<drawable name="notification_number_text_color">#ffffffff</drawable> <drawable name="notification_number_text_color">#ffffffff</drawable>
<drawable name="system_bar_background">@color/system_bar_background_opaque</drawable> <drawable name="system_bar_background">@color/system_bar_background_opaque</drawable>
<color name="system_bar_background_opaque">#ff000000</color> <color name="system_bar_background_opaque">#ff000000</color>

View File

@@ -1097,6 +1097,9 @@
<dimen name="ongoing_appops_dialog_side_padding">16dp</dimen> <dimen name="ongoing_appops_dialog_side_padding">16dp</dimen>
<dimen name="privacy_dialog_background_radius_large">12dp</dimen>
<dimen name="privacy_dialog_background_radius_small">4dp</dimen>
<!-- Size of media cards in the QSPanel carousel --> <!-- Size of media cards in the QSPanel carousel -->
<dimen name="qs_media_padding">16dp</dimen> <dimen name="qs_media_padding">16dp</dimen>
<dimen name="qs_media_album_radius">14dp</dimen> <dimen name="qs_media_album_radius">14dp</dimen>

View File

@@ -214,4 +214,8 @@
<item type="id" name="nssl_guideline" /> <item type="id" name="nssl_guideline" />
<item type="id" name="lock_icon" /> <item type="id" name="lock_icon" />
<item type="id" name="lock_icon_bg" /> <item type="id" name="lock_icon_bg" />
<!-- Privacy dialog -->
<item type="id" name="privacy_dialog_close_app_button" />
<item type="id" name="privacy_dialog_manage_app_button" />
</resources> </resources>

View File

@@ -3170,10 +3170,43 @@
<!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] --> <!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] -->
<string name="set_default_notes_app_toast_content">Set default notes app in Settings</string> <string name="set_default_notes_app_toast_content">Set default notes app in Settings</string>
<!-- <!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. -->
Label for a button that, when clicked, sends the user to the app store to install an app.
[CHAR LIMIT=64].
-->
<string name="install_app">Install app</string> <string name="install_app">Install app</string>
<!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] -->
<string name="privacy_dialog_title">Microphone &amp; Camera</string>
<!-- Subtitle of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_summary">Recent app use</string>
<!-- Label of the secondary button of the privacy dialog, used to check recent app usage of phone sensors [CHAR LIMIT=30] -->
<string name="privacy_dialog_more_button">See recent access</string>
<!-- Label of the primary button to dismiss the privacy dialog [CHAR LIMIT=20] -->
<string name="privacy_dialog_done_button">Done</string>
<!-- Description for expanding a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_expand_action">Expand and show options</string>
<!-- Description for collapsing a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_collapse_action">Collapse</string>
<!-- Label of a button of the privacy dialog to close an app actively using a phone sensor [CHAR LIMIT=50] -->
<string name="privacy_dialog_close_app_button">Close this app</string>
<!-- Message shown in the privacy dialog when an app actively using a phone sensor is closed [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_close_app_message"><xliff:g id="app_name" example="Gmail">%1$s</xliff:g> closed</string>
<!-- Label of a button of the privacy dialog to learn more of a service actively or recently using a phone sensor [CHAR LIMIT=50] -->
<string name="privacy_dialog_manage_service">Manage service</string>
<!-- Label of a button of the privacy dialog to manage permissions of an app actively or recently using a phone sensor [CHAR LIMIT=50] -->
<string name="privacy_dialog_manage_permissions">Manage access</string>
<!-- Label for active usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_active_call_usage">In use by phone call</string>
<!-- Label for recent usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_recent_call_usage">Recently used in phone call</string>
<!-- Label for active app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_active_app_usage">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
<!-- Label for recent app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_recent_app_usage">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
<!-- Label for active app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_active_app_usage_1">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
<!-- Label for recent app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_recent_app_usage_1">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
<!-- Label for active app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
<!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
<string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
</resources> </resources>

View File

@@ -1439,4 +1439,22 @@
<item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item> <item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item>
<item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item> <item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item>
</style> </style>
<style name="TextAppearance.PrivacyDialog.Item.Title"
parent="@android:style/TextAppearance.DeviceDefault.Medium">
<item name="android:textSize">14sp</item>
<item name="android:lineHeight">20sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
<style name="TextAppearance.PrivacyDialog.Item.Summary"
parent="@android:style/TextAppearance.DeviceDefault.Small">
<item name="android:textSize">14sp</item>
<item name="android:lineHeight">20sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
</style>
<style name="Theme.PrivacyDialog" parent="@style/Theme.SystemUI.Dialog">
<item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainer</item>
</style>
</resources> </resources>

View File

@@ -0,0 +1,367 @@
/*
* Copyright (C) 2023 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.privacy
import android.Manifest
import android.app.ActivityManager
import android.app.Dialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.UserHandle
import android.permission.PermissionGroupUsage
import android.permission.PermissionManager
import android.view.View
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.appops.AppOpsController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
import java.util.concurrent.Executor
import javax.inject.Inject
private val defaultDialogProvider =
object : PrivacyDialogControllerV2.DialogProvider {
override fun makeDialog(
context: Context,
list: List<PrivacyDialogV2.PrivacyElement>,
manageApp: (String, Int, Intent) -> Unit,
closeApp: (String, Int) -> Unit,
openPrivacyDashboard: () -> Unit
): PrivacyDialogV2 {
return PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
}
}
/**
* Controller for [PrivacyDialogV2].
*
* This controller shows and dismissed the dialog, as well as determining the information to show in
* it.
*/
@SysUISingleton
class PrivacyDialogControllerV2(
private val permissionManager: PermissionManager,
private val packageManager: PackageManager,
private val privacyItemController: PrivacyItemController,
private val userTracker: UserTracker,
private val activityStarter: ActivityStarter,
private val backgroundExecutor: Executor,
private val uiExecutor: Executor,
private val privacyLogger: PrivacyLogger,
private val keyguardStateController: KeyguardStateController,
private val appOpsController: AppOpsController,
private val uiEventLogger: UiEventLogger,
private val dialogLaunchAnimator: DialogLaunchAnimator,
private val dialogProvider: DialogProvider
) {
@Inject
constructor(
permissionManager: PermissionManager,
packageManager: PackageManager,
privacyItemController: PrivacyItemController,
userTracker: UserTracker,
activityStarter: ActivityStarter,
@Background backgroundExecutor: Executor,
@Main uiExecutor: Executor,
privacyLogger: PrivacyLogger,
keyguardStateController: KeyguardStateController,
appOpsController: AppOpsController,
uiEventLogger: UiEventLogger,
dialogLaunchAnimator: DialogLaunchAnimator
) : this(
permissionManager,
packageManager,
privacyItemController,
userTracker,
activityStarter,
backgroundExecutor,
uiExecutor,
privacyLogger,
keyguardStateController,
appOpsController,
uiEventLogger,
dialogLaunchAnimator,
defaultDialogProvider
)
private var dialog: Dialog? = null
private val onDialogDismissed =
object : PrivacyDialogV2.OnDialogDismissed {
override fun onDialogDismissed() {
privacyLogger.logPrivacyDialogDismissed()
uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
dialog = null
}
}
@WorkerThread
private fun closeApp(packageName: String, userId: Int) {
uiEventLogger.log(
PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
userId,
packageName
)
privacyLogger.logCloseAppFromDialog(packageName, userId)
ActivityManager.getService().stopAppForUser(packageName, userId)
}
@MainThread
private fun manageApp(packageName: String, userId: Int, navigationIntent: Intent) {
uiEventLogger.log(
PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
userId,
packageName
)
privacyLogger.logStartSettingsActivityFromDialog(packageName, userId)
startActivity(navigationIntent)
}
@MainThread
private fun openPrivacyDashboard() {
uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
privacyLogger.logStartPrivacyDashboardFromDialog()
startActivity(Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE))
}
@MainThread
private fun startActivity(navigationIntent: Intent) {
if (!keyguardStateController.isUnlocked) {
// If we are locked, hide the dialog so the user can unlock
dialog?.hide()
}
// startActivity calls internally startActivityDismissingKeyguard
activityStarter.startActivity(navigationIntent, true) {
if (ActivityManager.isStartResultSuccessful(it)) {
dismissDialog()
} else {
dialog?.show()
}
}
}
@WorkerThread
private fun getStartViewPermissionUsageIntent(
packageName: String,
permGroupName: String,
attributionTag: CharSequence?,
isAttributionSupported: Boolean
): Intent? {
if (attributionTag != null && isAttributionSupported) {
val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE)
intent.setPackage(packageName)
intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName)
intent.putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, arrayOf(attributionTag.toString()))
intent.putExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, true)
val resolveInfo =
packageManager.resolveActivity(intent, PackageManager.ResolveInfoFlags.of(0))
if (
resolveInfo?.activityInfo?.permission ==
Manifest.permission.START_VIEW_PERMISSION_USAGE
) {
intent.component = ComponentName(packageName, resolveInfo.activityInfo.name)
return intent
}
}
return null
}
fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent {
val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS)
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId))
return intent
}
@WorkerThread
private fun permGroupUsage(): List<PermissionGroupUsage> {
return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted)
}
/**
* Show the [PrivacyDialogV2]
*
* This retrieves the permission usage from [PermissionManager] and creates a new
* [PrivacyDialogV2] with a list of [PrivacyDialogV2.PrivacyElement] to show.
*
* This list will be filtered by [filterAndSelect]. Only types available by
* [PrivacyItemController] will be shown.
*
* @param context A context to use to create the dialog.
* @see filterAndSelect
*/
fun showDialog(context: Context, view: View? = null) {
dismissDialog()
backgroundExecutor.execute {
val usage = permGroupUsage()
val userInfos = userTracker.userProfiles
privacyLogger.logUnfilteredPermGroupUsage(usage)
val items =
usage.mapNotNull {
val userInfo =
userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
if (
isAvailable(it.permissionGroupName) && (userInfo != null || it.isPhoneCall)
) {
// Only try to get the app name if we actually need it
val appName =
if (it.isPhoneCall) {
""
} else {
getLabelForPackage(it.packageName, it.uid)
}
val userId = UserHandle.getUserId(it.uid)
val viewUsageIntent =
getStartViewPermissionUsageIntent(
it.packageName,
it.permissionGroupName,
it.attributionTag,
// attributionLabel is set only when subattribution policies
// are supported and satisfied
it.attributionLabel != null
)
PrivacyDialogV2.PrivacyElement(
permGroupToPrivacyType(it.permissionGroupName)!!,
it.packageName,
userId,
appName,
it.attributionTag,
it.attributionLabel,
it.proxyLabel,
it.lastAccessTimeMillis,
it.isActive,
it.isPhoneCall,
viewUsageIntent != null,
it.permissionGroupName,
viewUsageIntent
?: getDefaultManageAppPermissionsIntent(it.packageName, userId)
)
} else {
null
}
}
uiExecutor.execute {
val elements = filterAndSelect(items)
if (elements.isNotEmpty()) {
val d =
dialogProvider.makeDialog(
context,
elements,
this::manageApp,
this::closeApp,
this::openPrivacyDashboard
)
d.setShowForAllUsers(true)
d.addOnDismissListener(onDialogDismissed)
if (view != null) {
dialogLaunchAnimator.showFromView(d, view)
} else {
d.show()
}
privacyLogger.logShowDialogV2Contents(elements)
dialog = d
} else {
privacyLogger.logEmptyDialog()
}
}
}
}
/** Dismisses the dialog */
fun dismissDialog() {
dialog?.dismiss()
}
@WorkerThread
private fun getLabelForPackage(packageName: String, uid: Int): CharSequence {
return try {
packageManager
.getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid))
.loadLabel(packageManager)
} catch (_: PackageManager.NameNotFoundException) {
privacyLogger.logLabelNotFound(packageName)
packageName
}
}
private fun permGroupToPrivacyType(group: String): PrivacyType? {
return when (group) {
Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA
Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE
Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION
else -> null
}
}
private fun isAvailable(group: String): Boolean {
return when (group) {
Manifest.permission_group.CAMERA -> privacyItemController.micCameraAvailable
Manifest.permission_group.MICROPHONE -> privacyItemController.micCameraAvailable
Manifest.permission_group.LOCATION -> privacyItemController.locationAvailable
else -> false
}
}
/**
* Filters the list of elements to show.
*
* For each privacy type, it'll return all active elements. If there are no active elements,
* it'll return the most recent access
*/
private fun filterAndSelect(
list: List<PrivacyDialogV2.PrivacyElement>
): List<PrivacyDialogV2.PrivacyElement> {
return list
.groupBy { it.type }
.toSortedMap()
.flatMap { (_, elements) ->
val actives = elements.filter { it.isActive }
if (actives.isNotEmpty()) {
actives.sortedByDescending { it.lastActiveTimestamp }
} else {
elements.maxByOrNull { it.lastActiveTimestamp }?.let { listOf(it) }
?: emptyList()
}
}
}
/**
* Interface to create a [PrivacyDialogV2].
*
* Can be used to inject a mock creator.
*/
interface DialogProvider {
/** Create a [PrivacyDialogV2]. */
fun makeDialog(
context: Context,
list: List<PrivacyDialogV2.PrivacyElement>,
manageApp: (String, Int, Intent) -> Unit,
closeApp: (String, Int) -> Unit,
openPrivacyDashboard: () -> Unit
): PrivacyDialogV2
}
}

View File

@@ -18,13 +18,20 @@ package com.android.systemui.privacy
import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger import com.android.internal.logging.UiEventLogger
import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum { enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.") @UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.")
PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904), PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904),
@UiEvent(doc = "Privacy dialog is dismissed by user.") @UiEvent(doc = "Privacy dialog is dismissed by user.")
PRIVACY_DIALOG_DISMISSED(905); PRIVACY_DIALOG_DISMISSED(905),
@UiEvent(doc = "Privacy dialog item is clicked by user to close the app using a sensor.")
PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP(1396),
@UiEvent(doc = "Privacy dialog is clicked by user to see the privacy dashboard.")
PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD(1397);
override fun getId() = _id override fun getId() = _id
} }

View File

@@ -0,0 +1,539 @@
/*
* Copyright (C) 2023 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.privacy
import android.content.Context
import android.content.Intent
import android.content.pm.PackageItemInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.content.res.Resources.NotFoundException
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.WorkerThread
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.animation.ViewHierarchyAnimator
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.maybeForceFullscreen
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
/**
* Dialog to show ongoing and recent app ops element.
*
* @param context A context to create the dialog
* @param list list of elements to show in the dialog. The elements will show in the same order they
* appear in the list
* @param manageApp a callback to start an activity for a given package name, user id, and intent
* @param closeApp a callback to close an app for a given package name, user id
* @param openPrivacyDashboard a callback to open the privacy dashboard
* @see PrivacyDialogControllerV2
*/
class PrivacyDialogV2(
context: Context,
private val list: List<PrivacyElement>,
private val manageApp: (String, Int, Intent) -> Unit,
private val closeApp: (String, Int) -> Unit,
private val openPrivacyDashboard: () -> Unit
) : SystemUIDialog(context, R.style.Theme_PrivacyDialog) {
private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
private val dismissed = AtomicBoolean(false)
// Note: this will call the dialog create method during init
private val decorViewLayoutListener = maybeForceFullscreen()?.component2()
/**
* Add a listener that will be called when the dialog is dismissed.
*
* If the dialog has already been dismissed, the listener will be called immediately, in the
* same thread.
*/
fun addOnDismissListener(listener: OnDialogDismissed) {
if (dismissed.get()) {
listener.onDialogDismissed()
} else {
dismissListeners.add(WeakReference(listener))
}
}
override fun stop() {
dismissed.set(true)
val iterator = dismissListeners.iterator()
while (iterator.hasNext()) {
val el = iterator.next()
iterator.remove()
el.get()?.onDialogDismissed()
}
// Remove the layout change listener we may have added to the DecorView.
if (decorViewLayoutListener != null) {
window!!.decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window!!.setGravity(Gravity.CENTER)
setTitle(R.string.privacy_dialog_title)
setContentView(R.layout.privacy_dialog_v2)
val closeButton = requireViewById<Button>(R.id.privacy_dialog_close_button)
closeButton.setOnClickListener { dismiss() }
val moreButton = requireViewById<Button>(R.id.privacy_dialog_more_button)
moreButton.setOnClickListener { openPrivacyDashboard() }
val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
list.forEach { itemsContainer.addView(createView(it, itemsContainer)) }
}
private fun createView(element: PrivacyElement, itemsContainer: ViewGroup): View {
val itemCard =
LayoutInflater.from(context)
.inflate(R.layout.privacy_dialog_item_v2, itemsContainer, false) as ViewGroup
updateItemHeader(element, itemCard)
if (element.isPhoneCall) {
return itemCard
}
setItemExpansionBehavior(itemCard)
configureIndicatorActionButtons(element, itemCard)
return itemCard
}
private fun updateItemHeader(element: PrivacyElement, itemCard: View) {
val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
val permGroupLabel = context.packageManager.getDefaultPermGroupLabel(element.permGroupName)
val iconView = itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
val indicatorIcon = context.getPermGroupIcon(element.permGroupName)
updateIconView(iconView, indicatorIcon, element.isActive)
iconView.contentDescription = permGroupLabel
val titleView = itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_title)!!
titleView.text = permGroupLabel
titleView.contentDescription = permGroupLabel
val usageText = getUsageText(element)
val summaryView =
itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
summaryView.text = usageText
summaryView.contentDescription = usageText
}
private fun configureIndicatorActionButtons(element: PrivacyElement, itemCard: View) {
val expandedLayout =
itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header_expanded_layout)!!
val buttons: MutableList<View> = mutableListOf()
configureCloseAppButton(element, expandedLayout)?.also { buttons.add(it) }
buttons.add(configureManageButton(element, expandedLayout))
val backgroundColor = getBackgroundColor(element.isActive)
when (buttons.size) {
0 -> return
1 -> {
val background =
getMutableDrawable(R.drawable.privacy_dialog_background_large_top_large_bottom)
background.setTint(backgroundColor)
buttons[0].background = background
}
else -> {
val firstBackground =
getMutableDrawable(R.drawable.privacy_dialog_background_large_top_small_bottom)
val middleBackground =
getMutableDrawable(R.drawable.privacy_dialog_background_small_top_small_bottom)
val lastBackground =
getMutableDrawable(R.drawable.privacy_dialog_background_small_top_large_bottom)
firstBackground.setTint(backgroundColor)
middleBackground.setTint(backgroundColor)
lastBackground.setTint(backgroundColor)
buttons.forEach { it.background = middleBackground }
buttons.first().background = firstBackground
buttons.last().background = lastBackground
}
}
}
private fun configureCloseAppButton(element: PrivacyElement, expandedLayout: ViewGroup): View? {
if (element.isService || !element.isActive) {
return null
}
val closeAppButton =
window.layoutInflater.inflate(
R.layout.privacy_dialog_card_button,
expandedLayout,
false
) as Button
expandedLayout.addView(closeAppButton)
closeAppButton.id = R.id.privacy_dialog_close_app_button
closeAppButton.setText(R.string.privacy_dialog_close_app_button)
closeAppButton.setTextColor(getForegroundColor(true))
closeAppButton.tag = element
closeAppButton.setOnClickListener { v ->
v.tag?.let {
val element = it as PrivacyElement
closeApp(element.packageName, element.userId)
closeAppTransition(element.packageName, element.userId)
}
}
return closeAppButton
}
private fun closeAppTransition(packageName: String, userId: Int) {
val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
var shouldTransition = false
for (i in 0 until itemsContainer.getChildCount()) {
val itemCard = itemsContainer.getChildAt(i)
val button = itemCard.findViewById<Button>(R.id.privacy_dialog_close_app_button)
if (button == null || button.tag == null) {
continue
}
val element = button.tag as PrivacyElement
if (element.packageName != packageName || element.userId != userId) {
continue
}
itemCard.setEnabled(false)
val expandToggle =
itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
expandToggle.visibility = View.GONE
disableIndicatorCardUi(itemCard, element.applicationName)
val expandedLayout =
itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
if (expandedLayout.visibility == View.VISIBLE) {
expandedLayout.visibility = View.GONE
shouldTransition = true
}
}
if (shouldTransition) {
ViewHierarchyAnimator.animateNextUpdate(window!!.decorView)
}
}
private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View {
val manageButton =
window.layoutInflater.inflate(
R.layout.privacy_dialog_card_button,
expandedLayout,
false
) as Button
expandedLayout.addView(manageButton)
manageButton.id = R.id.privacy_dialog_manage_app_button
manageButton.setText(
if (element.isService) R.string.privacy_dialog_manage_service
else R.string.privacy_dialog_manage_permissions
)
manageButton.setTextColor(getForegroundColor(element.isActive))
manageButton.tag = element
manageButton.setOnClickListener { v ->
v.tag?.let {
val element = it as PrivacyElement
manageApp(element.packageName, element.userId, element.navigationIntent)
}
}
return manageButton
}
private fun disableIndicatorCardUi(itemCard: View, applicationName: CharSequence) {
val iconView = itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
val indicatorIcon = getMutableDrawable(R.drawable.privacy_dialog_check_icon)
updateIconView(iconView, indicatorIcon, false)
val closedAppText =
context.getString(R.string.privacy_dialog_close_app_message, applicationName)
val summaryView = itemCard.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
summaryView.text = closedAppText
summaryView.contentDescription = closedAppText
}
private fun setItemExpansionBehavior(itemCard: ViewGroup) {
val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
val expandToggle =
itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
expandToggle.visibility = View.VISIBLE
ViewCompat.replaceAccessibilityAction(
itemCard,
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
context.getString(R.string.privacy_dialog_expand_action),
null
)
val expandedLayout =
itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
expandedLayout.setOnClickListener {
// Stop clicks from propagating
}
itemCard.setOnClickListener {
if (expandedLayout.visibility == View.VISIBLE) {
expandedLayout.visibility = View.GONE
expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
ViewCompat.replaceAccessibilityAction(
it!!,
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
context.getString(R.string.privacy_dialog_expand_action),
null
)
} else {
expandedLayout.visibility = View.VISIBLE
expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up)
ViewCompat.replaceAccessibilityAction(
it!!,
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
context.getString(R.string.privacy_dialog_collapse_action),
null
)
}
ViewHierarchyAnimator.animateNextUpdate(
rootView = window!!.decorView,
excludedViews = setOf(expandedLayout)
)
}
}
private fun updateIconView(iconView: ImageView, indicatorIcon: Drawable, active: Boolean) {
indicatorIcon.setTint(getForegroundColor(active))
val backgroundIcon = getMutableDrawable(R.drawable.privacy_dialog_background_circle)
backgroundIcon.setTint(getBackgroundColor(active))
val backgroundSize =
context.resources.getDimension(R.dimen.ongoing_appops_dialog_circle_size).toInt()
val indicatorSize =
context.resources.getDimension(R.dimen.ongoing_appops_dialog_icon_size).toInt()
iconView.setImageDrawable(
constructLayeredIcon(indicatorIcon, indicatorSize, backgroundIcon, backgroundSize)
)
}
@ColorInt
private fun getForegroundColor(active: Boolean) =
Utils.getColorAttrDefaultColor(
context,
if (active) com.android.internal.R.attr.materialColorOnPrimaryFixed
else com.android.internal.R.attr.materialColorOnSurface
)
@ColorInt
private fun getBackgroundColor(active: Boolean) =
Utils.getColorAttrDefaultColor(
context,
if (active) com.android.internal.R.attr.materialColorPrimaryFixed
else com.android.internal.R.attr.materialColorSurfaceContainerHigh
)
private fun getMutableDrawable(@DrawableRes resId: Int) = context.getDrawable(resId)!!.mutate()
private fun getUsageText(element: PrivacyElement) =
if (element.isPhoneCall) {
val phoneCallResId =
if (element.isActive) R.string.privacy_dialog_active_call_usage
else R.string.privacy_dialog_recent_call_usage
context.getString(phoneCallResId)
} else if (element.attributionLabel == null && element.proxyLabel == null) {
val usageResId: Int =
if (element.isActive) R.string.privacy_dialog_active_app_usage
else R.string.privacy_dialog_recent_app_usage
context.getString(usageResId, element.applicationName)
} else if (element.attributionLabel == null || element.proxyLabel == null) {
val singleUsageResId: Int =
if (element.isActive) R.string.privacy_dialog_active_app_usage_1
else R.string.privacy_dialog_recent_app_usage_1
context.getString(
singleUsageResId,
element.applicationName,
element.attributionLabel ?: element.proxyLabel
)
} else {
val doubleUsageResId: Int =
if (element.isActive) R.string.privacy_dialog_active_app_usage_2
else R.string.privacy_dialog_recent_app_usage_2
context.getString(
doubleUsageResId,
element.applicationName,
element.attributionLabel,
element.proxyLabel
)
}
companion object {
private const val LOG_TAG = "PrivacyDialogV2"
private const val REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE"
/**
* Gets a permission group's icon from the system.
*
* @param groupName The name of the permission group whose icon we want
* @return The permission group's icon, the privacy_dialog_default_permission_icon icon if
* the group has no icon, or the group does not exist
*/
@WorkerThread
private fun Context.getPermGroupIcon(groupName: String): Drawable {
val groupInfo = packageManager.getGroupInfo(groupName)
if (groupInfo != null && groupInfo.icon != 0) {
val icon = packageManager.loadDrawable(groupInfo.packageName, groupInfo.icon)
if (icon != null) {
return icon
}
}
return getDrawable(R.drawable.privacy_dialog_default_permission_icon)!!.mutate()
}
/**
* Gets a permission group's label from the system.
*
* @param groupName The name of the permission group whose label we want
* @return The permission group's label, or the group name, if the group is invalid
*/
@WorkerThread
private fun PackageManager.getDefaultPermGroupLabel(groupName: String): CharSequence {
val groupInfo = getGroupInfo(groupName) ?: return groupName
return groupInfo.loadSafeLabel(
this,
0f,
TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
)
}
/**
* Get the [infos][PackageItemInfo] for the given permission group.
*
* @param groupName the group
* @return The info of permission group or null if the group does not have runtime
* permissions.
*/
@WorkerThread
private fun PackageManager.getGroupInfo(groupName: String): PackageItemInfo? {
try {
return getPermissionGroupInfo(groupName, 0)
} catch (e: NameNotFoundException) {
/* ignore */
}
try {
return getPermissionInfo(groupName, 0)
} catch (e: NameNotFoundException) {
/* ignore */
}
return null
}
@WorkerThread
private fun PackageManager.loadDrawable(pkg: String, @DrawableRes resId: Int): Drawable? {
return try {
getResourcesForApplication(pkg).getDrawable(resId, null)?.mutate()
} catch (e: NotFoundException) {
Log.w(LOG_TAG, "Couldn't get resource", e)
null
} catch (e: NameNotFoundException) {
Log.w(LOG_TAG, "Couldn't get resource", e)
null
}
}
private fun constructLayeredIcon(
icon: Drawable,
iconSize: Int,
background: Drawable,
backgroundSize: Int
): Drawable {
val layered = LayerDrawable(arrayOf(background, icon))
layered.setLayerSize(0, backgroundSize, backgroundSize)
layered.setLayerGravity(0, Gravity.CENTER)
layered.setLayerSize(1, iconSize, iconSize)
layered.setLayerGravity(1, Gravity.CENTER)
return layered
}
}
/** */
data class PrivacyElement(
val type: PrivacyType,
val packageName: String,
val userId: Int,
val applicationName: CharSequence,
val attributionTag: CharSequence?,
val attributionLabel: CharSequence?,
val proxyLabel: CharSequence?,
val lastActiveTimestamp: Long,
val isActive: Boolean,
val isPhoneCall: Boolean,
val isService: Boolean,
val permGroupName: String,
val navigationIntent: Intent
) {
private val builder = StringBuilder("PrivacyElement(")
init {
builder.append("type=${type.logName}")
builder.append(", packageName=$packageName")
builder.append(", userId=$userId")
builder.append(", appName=$applicationName")
if (attributionTag != null) {
builder.append(", attributionTag=$attributionTag")
}
if (attributionLabel != null) {
builder.append(", attributionLabel=$attributionLabel")
}
if (proxyLabel != null) {
builder.append(", proxyLabel=$proxyLabel")
}
builder.append(", lastActive=$lastActiveTimestamp")
if (isActive) {
builder.append(", active")
}
if (isPhoneCall) {
builder.append(", phoneCall")
}
if (isService) {
builder.append(", service")
}
builder.append(", permGroupName=$permGroupName")
builder.append(", navigationIntent=$navigationIntent)")
}
override fun toString(): String = builder.toString()
}
/** */
interface OnDialogDismissed {
fun onDialogDismissed()
}
}

View File

@@ -18,11 +18,12 @@ package com.android.systemui.privacy.logging
import android.icu.text.SimpleDateFormat import android.icu.text.SimpleDateFormat
import android.permission.PermissionGroupUsage import android.permission.PermissionGroupUsage
import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogMessage import com.android.systemui.log.core.LogMessage
import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.privacy.PrivacyDialog import com.android.systemui.privacy.PrivacyDialog
import com.android.systemui.privacy.PrivacyDialogV2
import com.android.systemui.privacy.PrivacyItem import com.android.systemui.privacy.PrivacyItem
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@@ -126,6 +127,14 @@ class PrivacyLogger @Inject constructor(
}) })
} }
fun logShowDialogV2Contents(contents: List<PrivacyDialogV2.PrivacyElement>) {
log(LogLevel.INFO, {
str1 = contents.toString()
}, {
"Privacy dialog shown. Contents: $str1"
})
}
fun logEmptyDialog() { fun logEmptyDialog() {
log(LogLevel.WARNING, {}, { log(LogLevel.WARNING, {}, {
"Trying to show an empty dialog" "Trying to show an empty dialog"
@@ -147,6 +156,23 @@ class PrivacyLogger @Inject constructor(
}) })
} }
fun logCloseAppFromDialog(packageName: String, userId: Int) {
log(LogLevel.INFO, {
str1 = packageName
int1 = userId
}, {
"Close app from dialog for packageName=$str1, userId=$int1"
})
}
fun logStartPrivacyDashboardFromDialog() {
log(LogLevel.INFO, {}, { "Start privacy dashboard from dialog" })
}
fun logLabelNotFound(packageName: String) {
log(LogLevel.WARNING, { str1 = packageName }, { "Label not found for: $str1" })
}
private fun listToString(list: List<PrivacyItem>): String { private fun listToString(list: List<PrivacyItem>): String {
return list.joinToString(separator = ", ", transform = PrivacyItem::log) return list.joinToString(separator = ", ", transform = PrivacyItem::log)
} }

View File

@@ -14,10 +14,13 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.appops.AppOpsController import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyChipEvent import com.android.systemui.privacy.PrivacyChipEvent
import com.android.systemui.privacy.PrivacyDialogController import com.android.systemui.privacy.PrivacyDialogController
import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItem import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger import com.android.systemui.privacy.logging.PrivacyLogger
@@ -49,6 +52,7 @@ class HeaderPrivacyIconsController @Inject constructor(
private val uiEventLogger: UiEventLogger, private val uiEventLogger: UiEventLogger,
@Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip, @Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip,
private val privacyDialogController: PrivacyDialogController, private val privacyDialogController: PrivacyDialogController,
private val privacyDialogControllerV2: PrivacyDialogControllerV2,
private val privacyLogger: PrivacyLogger, private val privacyLogger: PrivacyLogger,
@Named(SHADE_HEADER) private val iconContainer: StatusIconContainer, @Named(SHADE_HEADER) private val iconContainer: StatusIconContainer,
private val permissionManager: PermissionManager, private val permissionManager: PermissionManager,
@@ -58,7 +62,8 @@ class HeaderPrivacyIconsController @Inject constructor(
private val appOpsController: AppOpsController, private val appOpsController: AppOpsController,
private val broadcastDispatcher: BroadcastDispatcher, private val broadcastDispatcher: BroadcastDispatcher,
private val safetyCenterManager: SafetyCenterManager, private val safetyCenterManager: SafetyCenterManager,
private val deviceProvisionedController: DeviceProvisionedController private val deviceProvisionedController: DeviceProvisionedController,
private val featureFlags: FeatureFlags
) { ) {
var chipVisibilityListener: ChipVisibilityListener? = null var chipVisibilityListener: ChipVisibilityListener? = null
@@ -143,7 +148,11 @@ class HeaderPrivacyIconsController @Inject constructor(
// If the privacy chip is visible, it means there were some indicators // If the privacy chip is visible, it means there were some indicators
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK) uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
if (safetyCenterEnabled) { if (safetyCenterEnabled) {
showSafetyCenter() if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) {
privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip)
} else {
showSafetyCenter()
}
} else { } else {
privacyDialogController.showDialog(privacyChip.context) privacyDialogController.showDialog(privacyChip.context)
} }

View File

@@ -0,0 +1,825 @@
/*
* Copyright (C) 2021 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.privacy
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
import android.content.pm.ResolveInfo
import android.content.pm.UserInfo
import android.os.Process.SYSTEM_UID
import android.os.UserHandle
import android.permission.PermissionGroupUsage
import android.permission.PermissionManager
import android.testing.AndroidTestingRunner
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.appops.AppOpsController
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
class PrivacyDialogControllerV2Test : SysuiTestCase() {
companion object {
private const val USER_ID = 0
private const val ENT_USER_ID = 10
private const val TEST_PACKAGE_NAME = "test package name"
private const val TEST_ATTRIBUTION_TAG = "test attribution tag"
private const val TEST_PROXY_LABEL = "test proxy label"
private const val PERM_CAMERA = android.Manifest.permission_group.CAMERA
private const val PERM_MICROPHONE = android.Manifest.permission_group.MICROPHONE
private const val PERM_LOCATION = android.Manifest.permission_group.LOCATION
private val TEST_INTENT = Intent("test_intent_action")
}
@Mock
private lateinit var dialog: PrivacyDialogV2
@Mock
private lateinit var permissionManager: PermissionManager
@Mock
private lateinit var packageManager: PackageManager
@Mock
private lateinit var privacyItemController: PrivacyItemController
@Mock
private lateinit var userTracker: UserTracker
@Mock
private lateinit var activityStarter: ActivityStarter
@Mock
private lateinit var privacyLogger: PrivacyLogger
@Mock
private lateinit var keyguardStateController: KeyguardStateController
@Mock
private lateinit var appOpsController: AppOpsController
@Captor
private lateinit var dialogDismissedCaptor: ArgumentCaptor<PrivacyDialogV2.OnDialogDismissed>
@Captor
private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
@Captor
private lateinit var intentCaptor: ArgumentCaptor<Intent>
@Mock
private lateinit var uiEventLogger: UiEventLogger
@Mock
private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
private val uiExecutor = FakeExecutor(FakeSystemClock())
private lateinit var controller: PrivacyDialogControllerV2
private var nextUid: Int = 0
private val dialogProvider = object : PrivacyDialogControllerV2.DialogProvider {
var list: List<PrivacyDialogV2.PrivacyElement>? = null
var manageApp: ((String, Int, Intent) -> Unit)? = null
var closeApp: ((String, Int) -> Unit)? = null
var openPrivacyDashboard: (() -> Unit)? = null
override fun makeDialog(
context: Context,
list: List<PrivacyDialogV2.PrivacyElement>,
manageApp: (String, Int, Intent) -> Unit,
closeApp: (String, Int) -> Unit,
openPrivacyDashboard: () -> Unit
): PrivacyDialogV2 {
this.list = list
this.manageApp = manageApp
this.closeApp = closeApp
this.openPrivacyDashboard = openPrivacyDashboard
return dialog
}
}
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
nextUid = 0
setUpDefaultMockResponses()
controller = PrivacyDialogControllerV2(
permissionManager,
packageManager,
privacyItemController,
userTracker,
activityStarter,
backgroundExecutor,
uiExecutor,
privacyLogger,
keyguardStateController,
appOpsController,
uiEventLogger,
dialogLaunchAnimator,
dialogProvider
)
}
@After
fun tearDown() {
FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
dialogProvider.list = null
dialogProvider.manageApp = null
dialogProvider.closeApp = null
dialogProvider.openPrivacyDashboard = null
}
@Test
fun testMicMutedParameter() {
`when`(appOpsController.isMicMuted).thenReturn(true)
controller.showDialog(context)
backgroundExecutor.runAllReady()
verify(permissionManager).getIndicatorAppOpUsageData(true)
}
@Test
fun testPermissionManagerOnlyCalledInBackgroundThread() {
controller.showDialog(context)
verify(permissionManager, never()).getIndicatorAppOpUsageData(anyBoolean())
backgroundExecutor.runAllReady()
verify(permissionManager).getIndicatorAppOpUsageData(anyBoolean())
}
@Test
fun testPackageManagerOnlyCalledInBackgroundThread() {
val usage = createMockPermGroupUsage()
`when`(usage.isPhoneCall).thenReturn(false)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
verify(packageManager, never()).getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
backgroundExecutor.runAllReady()
verify(packageManager, atLeastOnce())
.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
}
@Test
fun testShowDialogShowsDialogWithoutView() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), anyBoolean())
verify(dialog).show()
}
@Test
fun testShowDialogShowsDialogWithView() {
val view = View(context)
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context, view)
exhaustExecutors()
verify(dialogLaunchAnimator).showFromView(dialog, view)
verify(dialog, never()).show()
}
@Test
fun testDontShowEmptyDialog() {
controller.showDialog(context)
exhaustExecutors()
verify(dialog, never()).show()
}
@Test
fun testHideDialogDismissesDialogIfShown() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
controller.dismissDialog()
verify(dialog).dismiss()
}
@Test
fun testHideDialogNoopIfNotShown() {
controller.dismissDialog()
verify(dialog, never()).dismiss()
}
@Test
fun testHideDialogNoopAfterDismissed() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
dialogDismissedCaptor.value.onDialogDismissed()
controller.dismissDialog()
verify(dialog, never()).dismiss()
}
@Test
fun testShowForAllUsers() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
verify(dialog).setShowForAllUsers(true)
}
@Test
fun testSingleElementInList() {
val usage = createMockPermGroupUsage(
packageName = TEST_PACKAGE_NAME,
uid = generateUidForUser(USER_ID),
permissionGroupName = PERM_CAMERA,
lastAccessTimeMillis = 5L,
isActive = true,
isPhoneCall = false,
attributionTag = null,
proxyLabel = TEST_PROXY_LABEL
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
assertThat(list.get(0).type).isEqualTo(PrivacyType.TYPE_CAMERA)
assertThat(list.get(0).packageName).isEqualTo(TEST_PACKAGE_NAME)
assertThat(list.get(0).userId).isEqualTo(USER_ID)
assertThat(list.get(0).applicationName).isEqualTo(TEST_PACKAGE_NAME)
assertThat(list.get(0).attributionTag).isNull()
assertThat(list.get(0).attributionLabel).isNull()
assertThat(list.get(0).proxyLabel).isEqualTo(TEST_PROXY_LABEL)
assertThat(list.get(0).lastActiveTimestamp).isEqualTo(5L)
assertThat(list.get(0).isActive).isTrue()
assertThat(list.get(0).isPhoneCall).isFalse()
assertThat(list.get(0).isService).isFalse()
assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA)
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
.isTrue()
}
}
private fun isIntentEqual(actual: Intent, expected: Intent): Boolean {
return actual.action == expected.action &&
actual.getStringExtra(Intent.EXTRA_PACKAGE_NAME) ==
expected.getStringExtra(Intent.EXTRA_PACKAGE_NAME) &&
actual.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle ==
expected.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle
}
@Test
fun testTwoElementsDifferentType_sorted() {
val usage_camera = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_camera",
permissionGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_microphone",
permissionGroupName = PERM_MICROPHONE
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
listOf(usage_microphone, usage_camera)
)
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
assertThat(list).hasSize(2)
assertThat(list.get(0).type.compareTo(list.get(1).type)).isLessThan(0)
}
}
@Test
fun testTwoElementsSameType_oneActive() {
val usage_active = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_active",
isActive = true
)
val usage_recent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_recent",
isActive = false
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
listOf(usage_recent, usage_active)
)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(1)
assertThat(dialogProvider.list?.get(0)?.isActive).isTrue()
}
@Test
fun testTwoElementsSameType_twoActive() {
val usage_active = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_active",
isActive = true,
lastAccessTimeMillis = 0L
)
val usage_active_moreRecent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_active_recent",
isActive = true,
lastAccessTimeMillis = 1L
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
listOf(usage_active, usage_active_moreRecent)
)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(2)
assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L)
assertThat(dialogProvider.list?.get(1)?.lastActiveTimestamp).isEqualTo(0L)
}
@Test
fun testManyElementsSameType_bothRecent() {
val usage_recent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_recent",
isActive = false,
lastAccessTimeMillis = 0L
)
val usage_moreRecent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_moreRecent",
isActive = false,
lastAccessTimeMillis = 1L
)
val usage_mostRecent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_mostRecent",
isActive = false,
lastAccessTimeMillis = 2L
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
listOf(usage_recent, usage_mostRecent, usage_moreRecent)
)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(1)
assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(2L)
}
@Test
fun testMicAndCameraDisabled() {
val usage_camera = createMockPermGroupUsage(
permissionGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
permissionGroupName = PERM_MICROPHONE
)
val usage_location = createMockPermGroupUsage(
permissionGroupName = PERM_LOCATION
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
listOf(usage_camera, usage_location, usage_microphone)
)
`when`(privacyItemController.micCameraAvailable).thenReturn(false)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(1)
assertThat(dialogProvider.list?.get(0)?.type).isEqualTo(PrivacyType.TYPE_LOCATION)
}
@Test
fun testLocationDisabled() {
val usage_camera = createMockPermGroupUsage(
permissionGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
permissionGroupName = PERM_MICROPHONE
)
val usage_location = createMockPermGroupUsage(
permissionGroupName = PERM_LOCATION
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
listOf(usage_camera, usage_location, usage_microphone)
)
`when`(privacyItemController.locationAvailable).thenReturn(false)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(2)
dialogProvider.list?.forEach {
assertThat(it.type).isNotEqualTo(PrivacyType.TYPE_LOCATION)
}
}
@Test
fun testAllIndicatorsAvailable() {
val usage_camera = createMockPermGroupUsage(
permissionGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
permissionGroupName = PERM_MICROPHONE
)
val usage_location = createMockPermGroupUsage(
permissionGroupName = PERM_LOCATION
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
listOf(usage_camera, usage_location, usage_microphone)
)
`when`(privacyItemController.micCameraAvailable).thenReturn(true)
`when`(privacyItemController.locationAvailable).thenReturn(true)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(3)
}
@Test
fun testNoIndicatorsAvailable() {
val usage_camera = createMockPermGroupUsage(
permissionGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
permissionGroupName = PERM_MICROPHONE
)
val usage_location = createMockPermGroupUsage(
permissionGroupName = PERM_LOCATION
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
listOf(usage_camera, usage_location, usage_microphone)
)
`when`(privacyItemController.micCameraAvailable).thenReturn(false)
`when`(privacyItemController.locationAvailable).thenReturn(false)
controller.showDialog(context)
exhaustExecutors()
verify(dialog, never()).show()
}
@Test
fun testNotCurrentUser() {
val usage_other = createMockPermGroupUsage(
uid = generateUidForUser(ENT_USER_ID + 1)
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean()))
.thenReturn(listOf(usage_other))
controller.showDialog(context)
exhaustExecutors()
verify(dialog, never()).show()
}
@Test
fun testStartActivitySuccess() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP)
verify(dialog).dismiss()
}
@Test
fun testStartActivityFailure() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
verify(dialog, never()).dismiss()
}
@Test
fun testCallOnSecondaryUser() {
// Calls happen in
val usage = createMockPermGroupUsage(uid = SYSTEM_UID, isPhoneCall = true)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
`when`(userTracker.userProfiles).thenReturn(listOf(
UserInfo(ENT_USER_ID, "", 0)
))
controller.showDialog(context)
exhaustExecutors()
verify(dialog).show()
}
@Test
fun testManageAppLogs() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
USER_ID, TEST_PACKAGE_NAME)
}
@Test
fun testCloseAppLogs() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.closeApp?.invoke(TEST_PACKAGE_NAME, USER_ID)
verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
USER_ID, TEST_PACKAGE_NAME)
}
@Test
fun testOpenPrivacyDashboardLogs() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.openPrivacyDashboard?.invoke()
verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
}
@Test
fun testDismissedDialogLogs() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
dialogDismissedCaptor.value.onDialogDismissed()
controller.dismissDialog()
verify(uiEventLogger, times(1)).log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
}
@Test
fun testDefaultIntent() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
.isTrue()
assertThat(list.get(0).isService).isFalse()
}
}
@Test
fun testDefaultIntentOnEnterpriseUser() {
val usage =
createMockPermGroupUsage(
uid = generateUidForUser(ENT_USER_ID),
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(
TEST_PACKAGE_NAME, ENT_USER_ID)))
.isTrue()
assertThat(list.get(0).isService).isFalse()
}
}
@Test
fun testDefaultIntentOnInvalidAttributionTag() {
val usage = createMockPermGroupUsage(
attributionTag = "INVALID_ATTRIBUTION_TAG",
proxyLabel = TEST_PROXY_LABEL
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
.isTrue()
assertThat(list.get(0).isService).isFalse()
}
}
@Test
fun testServiceIntentOnCorrectSubAttribution() {
val usage = createMockPermGroupUsage(
attributionTag = TEST_ATTRIBUTION_TAG,
attributionLabel = "TEST_LABEL"
)
val activityInfo = createMockActivityInfo()
val resolveInfo = createMockResolveInfo(activityInfo)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
`when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
.thenAnswer { resolveInfo }
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
val navigationIntent = list.get(0).navigationIntent!!
assertThat(navigationIntent.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_USAGE)
assertThat(navigationIntent.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME))
.isEqualTo(PERM_CAMERA)
assertThat(navigationIntent.getStringArrayExtra(Intent.EXTRA_ATTRIBUTION_TAGS))
.isEqualTo(arrayOf(TEST_ATTRIBUTION_TAG.toString()))
assertThat(navigationIntent.getBooleanExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, false))
.isTrue()
assertThat(list.get(0).isService).isTrue()
}
}
@Test
fun testDefaultIntentOnMissingAttributionLabel() {
val usage = createMockPermGroupUsage(
attributionTag = TEST_ATTRIBUTION_TAG
)
val activityInfo = createMockActivityInfo()
val resolveInfo = createMockResolveInfo(activityInfo)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
`when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
.thenAnswer { resolveInfo }
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
.isTrue()
assertThat(list.get(0).isService).isFalse()
}
}
@Test
fun testDefaultIntentOnIncorrectPermission() {
val usage = createMockPermGroupUsage(
attributionTag = TEST_ATTRIBUTION_TAG
)
val activityInfo = createMockActivityInfo(
permission = "INCORRECT_PERMISSION"
)
val resolveInfo = createMockResolveInfo(activityInfo)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
`when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
.thenAnswer { resolveInfo }
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
.isTrue()
assertThat(list.get(0).isService).isFalse()
}
}
private fun exhaustExecutors() {
FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
}
private fun setUpDefaultMockResponses() {
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(emptyList())
`when`(appOpsController.isMicMuted).thenReturn(false)
`when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
.thenAnswer { FakeApplicationInfo(it.getArgument(0)) }
`when`(privacyItemController.locationAvailable).thenReturn(true)
`when`(privacyItemController.micCameraAvailable).thenReturn(true)
`when`(userTracker.userProfiles).thenReturn(listOf(
UserInfo(USER_ID, "", 0),
UserInfo(ENT_USER_ID, "", UserInfo.FLAG_MANAGED_PROFILE)
))
`when`(keyguardStateController.isUnlocked).thenReturn(true)
}
private class FakeApplicationInfo(val label: CharSequence) : ApplicationInfo() {
override fun loadLabel(pm: PackageManager): CharSequence {
return label
}
}
private fun generateUidForUser(user: Int): Int {
return user * UserHandle.PER_USER_RANGE + nextUid++
}
private fun createMockResolveInfo(
activityInfo: ActivityInfo? = null
): ResolveInfo {
val resolveInfo = mock(ResolveInfo::class.java)
resolveInfo.activityInfo = activityInfo
return resolveInfo
}
private fun createMockActivityInfo(
permission: String = android.Manifest.permission.START_VIEW_PERMISSION_USAGE,
className: String = "TEST_CLASS_NAME"
): ActivityInfo {
val activityInfo = mock(ActivityInfo::class.java)
activityInfo.permission = permission
activityInfo.name = className
return activityInfo
}
private fun createMockPermGroupUsage(
packageName: String = TEST_PACKAGE_NAME,
uid: Int = generateUidForUser(USER_ID),
permissionGroupName: String = PERM_CAMERA,
lastAccessTimeMillis: Long = 0L,
isActive: Boolean = false,
isPhoneCall: Boolean = false,
attributionTag: CharSequence? = null,
attributionLabel: CharSequence? = null,
proxyLabel: CharSequence? = null
): PermissionGroupUsage {
val usage = mock(PermissionGroupUsage::class.java)
`when`(usage.packageName).thenReturn(packageName)
`when`(usage.uid).thenReturn(uid)
`when`(usage.permissionGroupName).thenReturn(permissionGroupName)
`when`(usage.lastAccessTimeMillis).thenReturn(lastAccessTimeMillis)
`when`(usage.isActive).thenReturn(isActive)
`when`(usage.isPhoneCall).thenReturn(isPhoneCall)
`when`(usage.attributionTag).thenReturn(attributionTag)
`when`(usage.attributionLabel).thenReturn(attributionLabel)
`when`(usage.proxyLabel).thenReturn(proxyLabel)
return usage
}
}

View File

@@ -0,0 +1,322 @@
/*
* Copyright (C) 2021 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.privacy
import android.content.Intent
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class PrivacyDialogV2Test : SysuiTestCase() {
companion object {
private const val TEST_PACKAGE_NAME = "test_pkg"
private const val TEST_USER_ID = 0
private const val TEST_PERM_GROUP = "test_perm_group"
private val TEST_INTENT = Intent("test_intent_action")
private fun createPrivacyElement(
type: PrivacyType = PrivacyType.TYPE_MICROPHONE,
packageName: String = TEST_PACKAGE_NAME,
userId: Int = TEST_USER_ID,
applicationName: CharSequence = "App",
attributionTag: CharSequence? = null,
attributionLabel: CharSequence? = null,
proxyLabel: CharSequence? = null,
lastActiveTimestamp: Long = 0L,
isActive: Boolean = false,
isPhoneCall: Boolean = false,
isService: Boolean = false,
permGroupName: String = TEST_PERM_GROUP,
navigationIntent: Intent = TEST_INTENT
) =
PrivacyDialogV2.PrivacyElement(
type,
packageName,
userId,
applicationName,
attributionTag,
attributionLabel,
proxyLabel,
lastActiveTimestamp,
isActive,
isPhoneCall,
isService,
permGroupName,
navigationIntent
)
}
@Mock private lateinit var manageApp: (String, Int, Intent) -> Unit
@Mock private lateinit var closeApp: (String, Int) -> Unit
@Mock private lateinit var openPrivacyDashboard: () -> Unit
private lateinit var dialog: PrivacyDialogV2
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@After
fun teardown() {
if (this::dialog.isInitialized) {
dialog.dismiss()
}
}
@Test
fun testManageAppCalledWithCorrectParams() {
val list = listOf(createPrivacyElement())
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
dialog.requireViewById<View>(R.id.privacy_dialog_manage_app_button).callOnClick()
verify(manageApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID, TEST_INTENT)
}
@Test
fun testCloseAppCalledWithCorrectParams() {
val list = listOf(createPrivacyElement(isActive = true))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
dialog.requireViewById<View>(R.id.privacy_dialog_close_app_button).callOnClick()
verify(closeApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID)
}
@Test
fun testCloseAppMissingForService() {
val list = listOf(createPrivacyElement(isActive = true, isService = true))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.findViewById<View>(R.id.privacy_dialog_manage_app_button)).isNotNull()
assertThat(dialog.findViewById<View>(R.id.privacy_dialog_close_app_button)).isNull()
}
@Test
fun testMoreButton() {
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
dialog.show()
dialog.requireViewById<View>(R.id.privacy_dialog_more_button).callOnClick()
verify(openPrivacyDashboard).invoke()
}
@Test
fun testCloseButton() {
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
dialog.addOnDismissListener(dismissListener)
dialog.show()
verify(dismissListener, never()).onDialogDismissed()
dialog.requireViewById<View>(R.id.privacy_dialog_close_button).callOnClick()
verify(dismissListener).onDialogDismissed()
}
@Test
fun testDismissListenerCalledOnDismiss() {
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
dialog.addOnDismissListener(dismissListener)
dialog.show()
verify(dismissListener, never()).onDialogDismissed()
dialog.dismiss()
verify(dismissListener).onDialogDismissed()
}
@Test
fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() {
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
dialog.show()
dialog.dismiss()
dialog.addOnDismissListener(dismissListener)
verify(dismissListener).onDialogDismissed()
}
@Test
fun testCorrectNumElements() {
val list =
listOf(
createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true),
createPrivacyElement()
)
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(
dialog.requireViewById<ViewGroup>(R.id.privacy_dialog_items_container).childCount
)
.isEqualTo(2)
}
@Test
fun testHeaderText() {
val list = listOf(createPrivacyElement())
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_title).text)
.isEqualTo(TEST_PERM_GROUP)
}
@Test
fun testUsingText() {
val list = listOf(createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("In use by App")
}
@Test
fun testRecentText() {
val list = listOf(createPrivacyElement())
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("Recently used by App")
}
@Test
fun testPhoneCall() {
val list = listOf(createPrivacyElement(isPhoneCall = true))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("Recently used in phone call")
}
@Test
fun testPhoneCallNotClickable() {
val list = listOf(createPrivacyElement(isPhoneCall = true))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.requireViewById<View>(R.id.privacy_dialog_item_card).isClickable)
.isFalse()
assertThat(
dialog
.requireViewById<View>(R.id.privacy_dialog_item_header_expand_toggle)
.visibility
)
.isEqualTo(View.GONE)
}
@Test
fun testProxyLabel() {
val list = listOf(createPrivacyElement(proxyLabel = "proxy label"))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("Recently used by App (proxy label)")
}
@Test
fun testSubattribution() {
val list =
listOf(
createPrivacyElement(
attributionLabel = "For subattribution",
isActive = true,
isService = true
)
)
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("In use by App (For subattribution)")
}
@Test
fun testSubattributionAndProxyLabel() {
val list =
listOf(
createPrivacyElement(
attributionLabel = "For subattribution",
proxyLabel = "proxy label",
isActive = true
)
)
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("In use by App (For subattribution \u2022 proxy label)")
}
@Test
fun testDialogHasTitle() {
val list = listOf(createPrivacyElement())
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.window?.attributes?.title).isEqualTo("Microphone & Camera")
}
@Test
fun testDialogIsFullscreen() {
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(dialog.window?.attributes?.width).isEqualTo(MATCH_PARENT)
assertThat(dialog.window?.attributes?.height).isEqualTo(MATCH_PARENT)
}
}

View File

@@ -13,9 +13,12 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.appops.AppOpsController import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyDialogController import com.android.systemui.privacy.PrivacyDialogController
import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItemController import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -24,6 +27,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before import org.junit.Before
@@ -54,6 +58,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
@Mock @Mock
private lateinit var privacyDialogController: PrivacyDialogController private lateinit var privacyDialogController: PrivacyDialogController
@Mock @Mock
private lateinit var privacyDialogControllerV2: PrivacyDialogControllerV2
@Mock
private lateinit var privacyLogger: PrivacyLogger private lateinit var privacyLogger: PrivacyLogger
@Mock @Mock
private lateinit var iconContainer: StatusIconContainer private lateinit var iconContainer: StatusIconContainer
@@ -69,6 +75,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
private lateinit var safetyCenterManager: SafetyCenterManager private lateinit var safetyCenterManager: SafetyCenterManager
@Mock @Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock
private lateinit var featureFlags: FeatureFlags
private val uiExecutor = FakeExecutor(FakeSystemClock()) private val uiExecutor = FakeExecutor(FakeSystemClock())
private val backgroundExecutor = FakeExecutor(FakeSystemClock()) private val backgroundExecutor = FakeExecutor(FakeSystemClock())
@@ -94,6 +102,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
uiEventLogger, uiEventLogger,
privacyChip, privacyChip,
privacyDialogController, privacyDialogController,
privacyDialogControllerV2,
privacyLogger, privacyLogger,
iconContainer, iconContainer,
permissionManager, permissionManager,
@@ -103,7 +112,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
appOpsController, appOpsController,
broadcastDispatcher, broadcastDispatcher,
safetyCenterManager, safetyCenterManager,
deviceProvisionedController deviceProvisionedController,
featureFlags
) )
backgroundExecutor.runAllReady() backgroundExecutor.runAllReady()
@@ -154,17 +164,34 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
} }
@Test @Test
fun testPrivacyChipClicked() { fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterDisabled() {
whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false) whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
controller.onParentVisible() controller.onParentVisible()
val captor = argumentCaptor<View.OnClickListener>() val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor)) verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip) captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java)) verify(privacyDialogController).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
.showDialog(any(Context::class.java), any(View::class.java))
} }
@Test @Test
fun testSafetyCenterFlag() { fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterDisabled() {
whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
controller.onParentVisible()
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
.showDialog(any(Context::class.java), any(View::class.java))
}
@Test
fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterEnabled() {
whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
val receiverCaptor = argumentCaptor<BroadcastReceiver>() val receiverCaptor = argumentCaptor<BroadcastReceiver>()
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true) whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor), verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
@@ -179,6 +206,28 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
verify(privacyChip).setOnClickListener(capture(captor)) verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip) captor.value.onClick(privacyChip)
verify(privacyDialogController, never()).showDialog(any(Context::class.java)) verify(privacyDialogController, never()).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
.showDialog(any(Context::class.java), any(View::class.java))
}
@Test
fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterEnabled() {
whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
val receiverCaptor = argumentCaptor<BroadcastReceiver>()
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
any(), any(), nullable(), anyInt(), nullable())
receiverCaptor.value.onReceive(
context,
Intent(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED)
)
backgroundExecutor.runAllReady()
controller.onParentVisible()
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogControllerV2).showDialog(any(Context::class.java), eq(privacyChip))
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
} }
@Test @Test