New privacy dialog

go/new-sysui-privacy-dialog

Bug: 283456173
Test: atest SystemUITests
Change-Id: I63e6200e418fff94acc8dd7eaa551d15e049ba8a
This commit is contained in:
Giulio Fiscella
2023-02-17 17:12:21 +00:00
parent 54649e344c
commit aaeae64045
29 changed files with 1663 additions and 693 deletions

View File

@@ -32,11 +32,10 @@ import android.view.ViewRootImpl
import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
import android.widget.FrameLayout
import com.android.app.animation.Interpolators
import com.android.internal.jank.InteractionJankMonitor
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 kotlin.math.roundToInt
@@ -622,96 +621,12 @@ private class AnimatedDialog(
viewGroupWithBackground
} else {
// 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(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)
val (dialogContentWithBackground, decorViewLayoutListener) =
dialog.maybeForceFullscreen()!!
this.decorViewLayoutListener = decorViewLayoutListener
dialogContentWithBackground
}
this.dialogContentWithBackground = dialogContentWithBackground
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.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import android.window.OnBackInvokedDispatcher
import com.android.systemui.animation.back.BackAnimationSpec
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.onBackAnimationCallbackFrom
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].
@@ -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.
*/
-->
<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="system_bar_background">@color/system_bar_background_opaque</drawable>
<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="privacy_dialog_background_radius_large">12dp</dimen>
<dimen name="privacy_dialog_background_radius_small">4dp</dimen>
<!-- Size of media cards in the QSPanel carousel -->
<dimen name="qs_media_padding">16dp</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="lock_icon" />
<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>

View File

@@ -3167,10 +3167,43 @@
<!--- 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>
<!--
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>
<!-- 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>

View File

@@ -1439,4 +1439,22 @@
<item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item>
<item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item>
</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>

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 The Android Open Source Project
* 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.
@@ -26,11 +26,11 @@ import android.content.pm.PackageManager
import android.os.UserHandle
import android.permission.PermissionGroupUsage
import android.permission.PermissionManager
import android.util.Log
import android.view.View
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
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
@@ -42,15 +42,18 @@ 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>,
starter: (String, Int, CharSequence?, Intent?) -> Unit
): PrivacyDialogV2 {
return PrivacyDialogV2(context, list, starter)
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].
@@ -71,7 +74,8 @@ class PrivacyDialogControllerV2(
private val keyguardStateController: KeyguardStateController,
private val appOpsController: AppOpsController,
private val uiEventLogger: UiEventLogger,
@VisibleForTesting private val dialogProvider: DialogProvider
private val dialogLaunchAnimator: DialogLaunchAnimator,
private val dialogProvider: DialogProvider
) {
@Inject
@@ -86,57 +90,72 @@ class PrivacyDialogControllerV2(
privacyLogger: PrivacyLogger,
keyguardStateController: KeyguardStateController,
appOpsController: AppOpsController,
uiEventLogger: UiEventLogger
uiEventLogger: UiEventLogger,
dialogLaunchAnimator: DialogLaunchAnimator
) : this(
permissionManager,
packageManager,
privacyItemController,
userTracker,
activityStarter,
backgroundExecutor,
uiExecutor,
privacyLogger,
keyguardStateController,
appOpsController,
uiEventLogger,
defaultDialogProvider
permissionManager,
packageManager,
privacyItemController,
userTracker,
activityStarter,
backgroundExecutor,
uiExecutor,
privacyLogger,
keyguardStateController,
appOpsController,
uiEventLogger,
dialogLaunchAnimator,
defaultDialogProvider
)
companion object {
private const val TAG = "PrivacyDialogController"
}
private var dialog: Dialog? = null
private val onDialogDismissed = object : PrivacyDialogV2.OnDialogDismissed {
override fun onDialogDismissed() {
privacyLogger.logPrivacyDialogDismissed()
uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
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 startActivity(
packageName: String,
userId: Int,
attributionTag: CharSequence?,
navigationIntent: Intent?
) {
val intent = if (navigationIntent == null) {
getDefaultManageAppPermissionsIntent(packageName, userId)
} else {
navigationIntent
}
uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
userId, packageName)
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(intent, true) {
activityStarter.startActivity(navigationIntent, true) {
if (ActivityManager.isStartResultSuccessful(it)) {
dismissDialog()
} else {
@@ -146,31 +165,29 @@ class PrivacyDialogControllerV2(
}
@WorkerThread
private fun getManagePermissionIntent(
private fun getStartViewPermissionUsageIntent(
packageName: String,
userId: Int,
permGroupName: CharSequence,
permGroupName: String,
attributionTag: CharSequence?,
isAttributionSupported: Boolean
): Intent
{
lateinit var intent: Intent
): Intent? {
if (attributionTag != null && isAttributionSupported) {
intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE)
val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE)
intent.setPackage(packageName)
intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName.toString())
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 != null && resolveInfo.activityInfo != null &&
resolveInfo.activityInfo.permission ==
android.Manifest.permission.START_VIEW_PERMISSION_USAGE) {
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 getDefaultManageAppPermissionsIntent(packageName, userId)
return null
}
fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent {
@@ -197,73 +214,84 @@ class PrivacyDialogControllerV2(
* @param context A context to use to create the dialog.
* @see filterAndSelect
*/
fun showDialog(context: Context) {
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 type = filterType(permGroupToPrivacyType(it.permissionGroupName))
val userInfo = userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
if (userInfo != null || it.isPhoneCall) {
type?.let { t ->
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 appName =
if (it.isPhoneCall) {
""
} else {
getLabelForPackage(it.packageName, it.uid)
}
val userId = UserHandle.getUserId(it.uid)
PrivacyDialogV2.PrivacyElement(
t,
val viewUsageIntent =
getStartViewPermissionUsageIntent(
it.packageName,
userId,
appName,
it.attributionTag,
it.attributionLabel,
it.proxyLabel,
it.lastAccessTimeMillis,
it.isActive,
// If there's no user info, we're in a phoneCall in secondary user
userInfo?.isManagedProfile ?: false,
it.isPhoneCall,
it.permissionGroupName,
getManagePermissionIntent(
it.packageName,
userId,
it.permissionGroupName,
it.attributionTag,
// attributionLabel is set only when subattribution policies
// are supported and satisfied
it.attributionLabel != null
)
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
}
} else {
// No matching user or phone call
null
}
}
uiExecutor.execute {
val elements = filterAndSelect(items)
if (elements.isNotEmpty()) {
val d = dialogProvider.makeDialog(context, elements, this::startActivity)
val d =
dialogProvider.makeDialog(
context,
elements,
this::manageApp,
this::closeApp,
this::openPrivacyDashboard
)
d.setShowForAllUsers(true)
d.addOnDismissListener(onDialogDismissed)
d.show()
if (view != null) {
dialogLaunchAnimator.showFromView(d, view)
} else {
d.show()
}
privacyLogger.logShowDialogV2Contents(elements)
dialog = d
} else {
Log.w(TAG, "Trying to show empty dialog")
privacyLogger.logEmptyDialog()
}
}
}
}
/**
* Dismisses the dialog
*/
/** Dismisses the dialog */
fun dismissDialog() {
dialog?.dismiss()
}
@@ -275,7 +303,7 @@ class PrivacyDialogControllerV2(
.getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid))
.loadLabel(packageManager)
} catch (_: PackageManager.NameNotFoundException) {
Log.w(TAG, "Label not found for: $packageName")
privacyLogger.logLabelNotFound(packageName)
packageName
}
}
@@ -289,16 +317,12 @@ class PrivacyDialogControllerV2(
}
}
private fun filterType(type: PrivacyType?): PrivacyType? {
return type?.let {
if ((it == PrivacyType.TYPE_CAMERA || it == PrivacyType.TYPE_MICROPHONE) &&
privacyItemController.micCameraAvailable) {
it
} else if (it == PrivacyType.TYPE_LOCATION && privacyItemController.locationAvailable) {
it
} 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
}
}
@@ -311,16 +335,18 @@ class PrivacyDialogControllerV2(
private fun filterAndSelect(
list: List<PrivacyDialogV2.PrivacyElement>
): List<PrivacyDialogV2.PrivacyElement> {
return list.groupBy { it.type }.toSortedMap().flatMap { (_, elements) ->
val actives = elements.filter { it.active }
if (actives.isNotEmpty()) {
actives.sortedByDescending { it.lastActiveTimestamp }
} else {
elements.maxByOrNull { it.lastActiveTimestamp }?.let {
listOf(it)
} ?: emptyList()
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()
}
}
}
}
/**
@@ -329,13 +355,13 @@ class PrivacyDialogControllerV2(
* Can be used to inject a mock creator.
*/
interface DialogProvider {
/**
* Create a [PrivacyDialogV2].
*/
/** Create a [PrivacyDialogV2]. */
fun makeDialog(
context: Context,
list: List<PrivacyDialogV2.PrivacyElement>,
starter: (String, Int, CharSequence?, Intent?) -> Unit
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.UiEventLogger
import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.")
PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904),
@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
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 The Android Open Source Project
* 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.
@@ -18,63 +18,58 @@ 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.view.WindowInsets
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 usage.
* Dialog to show ongoing and recent app ops element.
*
* @see PrivacyDialogV2Controller
* @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 activityStarter a callback to start an activity for a given package name, user id, attributionTag and intent
* 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>,
activityStarter: (String, Int, CharSequence?, Intent?) -> Unit
) : SystemUIDialog(context, R.style.PrivacyDialog) {
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)
private val iconColorSolid = Utils.getColorAttrDefaultColor(
this.context, com.android.internal.R.attr.colorPrimary
)
private val enterpriseText = " ${context.getString(R.string.ongoing_privacy_dialog_enterprise)}"
private val phonecall = context.getString(R.string.ongoing_privacy_dialog_phonecall)
private lateinit var rootView: ViewGroup
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window?.apply {
attributes.fitInsetsTypes = attributes.fitInsetsTypes or WindowInsets.Type.statusBars()
attributes.receiveInsetsIgnoringZOrder = true
setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL)
}
setTitle(R.string.ongoing_privacy_dialog_a11y_title)
setContentView(R.layout.privacy_dialog)
rootView = requireViewById<ViewGroup>(R.id.root)
list.forEach {
rootView.addView(createView(it))
}
}
// 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.
@@ -98,85 +93,397 @@ class PrivacyDialogV2(
iterator.remove()
el.get()?.onDialogDismissed()
}
// Remove the layout change listener we may have added to the DecorView.
if (decorViewLayoutListener != null) {
window!!.decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
}
}
private fun createView(element: PrivacyElement): View {
val newView = LayoutInflater.from(context).inflate(
R.layout.privacy_dialog_item, rootView, false
) as ViewGroup
val d = getDrawableForType(element.type)
d.findDrawableByLayerId(R.id.icon).setTint(iconColorSolid)
newView.requireViewById<ImageView>(R.id.icon).apply {
setImageDrawable(d)
contentDescription = element.type.getName(context)
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
}
val stringId = getStringIdForState(element.active)
val app = if (element.phoneCall) phonecall else element.applicationName
val appName = if (element.enterprise) {
TextUtils.concat(app, enterpriseText)
} else {
app
}
val firstLine = context.getString(stringId, appName)
val finalText = getFinalText(firstLine, element.attributionLabel, element.proxyLabel)
newView.requireViewById<TextView>(R.id.text).text = finalText
if (element.phoneCall) {
newView.requireViewById<View>(R.id.chevron).visibility = View.GONE
}
newView.apply {
setTag(element)
if (!element.phoneCall) {
setOnClickListener(clickListener)
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
}
}
return newView
}
private fun getFinalText(
firstLine: CharSequence,
attributionLabel: CharSequence?,
proxyLabel: CharSequence?
): CharSequence {
var dialogText: CharSequence? = null
if (attributionLabel != null && proxyLabel != null) {
dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_proxy_label,
attributionLabel, proxyLabel)
} else if (attributionLabel != null) {
dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_label,
attributionLabel)
} else if (proxyLabel != null) {
dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_text,
proxyLabel)
private fun configureCloseAppButton(element: PrivacyElement, expandedLayout: ViewGroup): View? {
if (element.isService || !element.isActive) {
return null
}
return if (dialogText != null) TextUtils.concat(firstLine, " ", dialogText) else firstLine
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 getStringIdForState(active: Boolean): Int {
return if (active) {
R.string.ongoing_privacy_dialog_using_op
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 {
R.string.ongoing_privacy_dialog_recent_op
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
}
}
private fun getDrawableForType(type: PrivacyType): LayerDrawable {
return context.getDrawable(when (type) {
PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location
PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera
PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone
PrivacyType.TYPE_MEDIA_PROJECTION -> R.drawable.privacy_item_circle_media_projection
}) as LayerDrawable
}
private val clickListener = View.OnClickListener { v ->
v.tag?.let {
val element = it as PrivacyElement
activityStarter(element.packageName, element.userId,
element.attributionTag, element.navigationIntent)
}
}
/** */
/** */
data class PrivacyElement(
val type: PrivacyType,
val packageName: String,
@@ -186,11 +493,11 @@ class PrivacyDialogV2(
val attributionLabel: CharSequence?,
val proxyLabel: CharSequence?,
val lastActiveTimestamp: Long,
val active: Boolean,
val enterprise: Boolean,
val phoneCall: Boolean,
val permGroupName: CharSequence,
val navigationIntent: Intent?
val isActive: Boolean,
val isPhoneCall: Boolean,
val isService: Boolean,
val permGroupName: String,
val navigationIntent: Intent
) {
private val builder = StringBuilder("PrivacyElement(")
@@ -209,26 +516,24 @@ class PrivacyDialogV2(
builder.append(", proxyLabel=$proxyLabel")
}
builder.append(", lastActive=$lastActiveTimestamp")
if (active) {
if (isActive) {
builder.append(", active")
}
if (enterprise) {
builder.append(", enterprise")
}
if (phoneCall) {
if (isPhoneCall) {
builder.append(", phoneCall")
}
builder.append(", permGroupName=$permGroupName)")
if (navigationIntent != null) {
builder.append(", navigationIntent=$navigationIntent")
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,10 +18,10 @@ package com.android.systemui.privacy.logging
import android.icu.text.SimpleDateFormat
import android.permission.PermissionGroupUsage
import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
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.PrivacyDialogV2
import com.android.systemui.privacy.PrivacyItem
@@ -156,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 {
return list.joinToString(separator = ", ", transform = PrivacyItem::log)
}
@@ -167,4 +184,4 @@ class PrivacyLogger @Inject constructor(
) {
buffer.log(TAG, logLevel, initializer, printer)
}
}
}

View File

@@ -149,7 +149,7 @@ class HeaderPrivacyIconsController @Inject constructor(
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
if (safetyCenterEnabled) {
if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) {
privacyDialogControllerV2.showDialog(privacyChip.context)
privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip)
} else {
showSafetyCenter()
}

View File

@@ -30,9 +30,11 @@ 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
@@ -40,6 +42,7 @@ 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
@@ -48,7 +51,6 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
@@ -77,6 +79,8 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
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
@@ -105,6 +109,8 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
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())
@@ -113,15 +119,21 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
private val dialogProvider = object : PrivacyDialogControllerV2.DialogProvider {
var list: List<PrivacyDialogV2.PrivacyElement>? = null
var starter: ((String, Int, CharSequence?, Intent?) -> Unit)? = 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>,
starter: (String, Int, CharSequence?, Intent?) -> Unit
manageApp: (String, Int, Intent) -> Unit,
closeApp: (String, Int) -> Unit,
openPrivacyDashboard: () -> Unit
): PrivacyDialogV2 {
this.list = list
this.starter = starter
this.manageApp = manageApp
this.closeApp = closeApp
this.openPrivacyDashboard = openPrivacyDashboard
return dialog
}
}
@@ -144,6 +156,7 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
keyguardStateController,
appOpsController,
uiEventLogger,
dialogLaunchAnimator,
dialogProvider
)
}
@@ -152,7 +165,9 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
fun tearDown() {
FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
dialogProvider.list = null
dialogProvider.starter = null
dialogProvider.manageApp = null
dialogProvider.closeApp = null
dialogProvider.openPrivacyDashboard = null
}
@Test
@@ -186,16 +201,30 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
}
@Test
fun testShowDialogShowsDialog() {
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)
@@ -271,9 +300,9 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
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).active).isTrue()
assertThat(list.get(0).phoneCall).isFalse()
assertThat(list.get(0).enterprise).isFalse()
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)))
@@ -330,7 +359,7 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(1)
assertThat(dialogProvider.list?.get(0)?.active).isTrue()
assertThat(dialogProvider.list?.get(0)?.isActive).isTrue()
}
@Test
@@ -481,20 +510,6 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
verify(dialog, never()).show()
}
@Test
fun testEnterpriseUser() {
val usage_enterprise = createMockPermGroupUsage(
uid = generateUidForUser(ENT_USER_ID)
)
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean()))
.thenReturn(listOf(usage_enterprise))
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list?.single()?.enterprise).isTrue()
}
@Test
fun testNotCurrentUser() {
val usage_other = createMockPermGroupUsage(
@@ -509,39 +524,6 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
verify(dialog, never()).show()
}
@Test
fun testStartActivityCorrectIntent() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, USER_ID, null, null)
verify(activityStarter)
.startActivity(capture(intentCaptor), eq(true), any<ActivityStarter.Callback>())
assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_MANAGE_APP_PERMISSIONS)
assertThat(intentCaptor.value.getStringExtra(Intent.EXTRA_PACKAGE_NAME))
.isEqualTo(TEST_PACKAGE_NAME)
assertThat(intentCaptor.value.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle)
.isEqualTo(UserHandle.of(USER_ID))
}
@Test
fun testStartActivityCorrectIntent_enterpriseUser() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, ENT_USER_ID, null, null)
verify(activityStarter)
.startActivity(capture(intentCaptor), eq(true), any<ActivityStarter.Callback>())
assertThat(intentCaptor.value.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle)
.isEqualTo(UserHandle.of(ENT_USER_ID))
}
@Test
fun testStartActivitySuccess() {
val usage = createMockPermGroupUsage()
@@ -549,7 +531,7 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
controller.showDialog(context)
exhaustExecutors()
dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, USER_ID, null, null)
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)
@@ -564,7 +546,7 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
controller.showDialog(context)
exhaustExecutors()
dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, USER_ID, null, null)
dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
@@ -588,17 +570,40 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
}
@Test
fun testStartActivityLogs() {
fun testManageAppLogs() {
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, USER_ID, null, null)
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()
@@ -616,14 +621,44 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
}
@Test
fun testInvalidAttributionTag() {
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(
packageName = TEST_PACKAGE_NAME,
uid = generateUidForUser(USER_ID),
permissionGroupName = PERM_CAMERA,
lastAccessTimeMillis = 5L,
isActive = true,
isPhoneCall = false,
attributionTag = "INVALID_ATTRIBUTION_TAG",
proxyLabel = TEST_PROXY_LABEL
)
@@ -633,26 +668,15 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
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).isEqualTo("INVALID_ATTRIBUTION_TAG")
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).active).isTrue()
assertThat(list.get(0).phoneCall).isFalse()
assertThat(list.get(0).enterprise).isFalse()
assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA)
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
.isTrue()
assertThat(list.get(0).isService).isFalse()
}
}
@Test
fun testCorrectIntentSubAttribution() {
fun testServiceIntentOnCorrectSubAttribution() {
val usage = createMockPermGroupUsage(
attributionTag = TEST_ATTRIBUTION_TAG,
attributionLabel = "TEST_LABEL"
@@ -675,6 +699,7 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
.isEqualTo(arrayOf(TEST_ATTRIBUTION_TAG.toString()))
assertThat(navigationIntent.getBooleanExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, false))
.isTrue()
assertThat(list.get(0).isService).isTrue()
}
}
@@ -696,6 +721,7 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
.isTrue()
assertThat(list.get(0).isService).isFalse()
}
}
@@ -719,6 +745,7 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() {
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
.isTrue()
assertThat(list.get(0).isService).isFalse()
}
}

View File

@@ -16,10 +16,12 @@
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
@@ -34,8 +36,6 @@ import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import android.content.Intent
import android.text.TextUtils
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -46,10 +46,44 @@ class PrivacyDialogV2Test : SysuiTestCase() {
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 starter: (String, Int, CharSequence?, Intent?) -> Unit
@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
@@ -65,340 +99,224 @@ class PrivacyDialogV2Test : SysuiTestCase() {
}
@Test
fun testStarterCalledWithCorrectParams() {
val list = listOf(
PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
false,
false,
false,
TEST_PERM_GROUP,
null
)
)
dialog = PrivacyDialogV2(context, list, starter)
fun testManageAppCalledWithCorrectParams() {
val list = listOf(createPrivacyElement())
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
dialog.requireViewById<View>(R.id.privacy_item).callOnClick()
verify(starter).invoke(TEST_PACKAGE_NAME, TEST_USER_ID, null, null)
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(), starter)
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(), starter)
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(
PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_CAMERA,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
true,
false,
false,
TEST_PERM_GROUP,
null
),
PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
false,
false,
false,
TEST_PERM_GROUP,
null
)
)
dialog = PrivacyDialogV2(context, list, starter)
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.root).childCount).isEqualTo(2)
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 element = PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_CAMERA,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
true,
false,
false,
TEST_PERM_GROUP,
null
)
val list = listOf(createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
val list = listOf(element)
dialog = PrivacyDialogV2(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text).isEqualTo(
context.getString(
R.string.ongoing_privacy_dialog_using_op,
element.applicationName,
element.type.getName(context)
)
)
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("In use by App")
}
@Test
fun testRecentText() {
val element = PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
false,
false,
false,
TEST_PERM_GROUP,
null
)
val list = listOf(createPrivacyElement())
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
val list = listOf(element)
dialog = PrivacyDialogV2(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text).isEqualTo(
context.getString(
R.string.ongoing_privacy_dialog_recent_op,
element.applicationName,
element.type.getName(context)
)
)
}
@Test
fun testEnterprise() {
val element = PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
false,
true,
false,
TEST_PERM_GROUP,
null
)
val list = listOf(element)
dialog = PrivacyDialogV2(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
context.getString(R.string.ongoing_privacy_dialog_enterprise)
)
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("Recently used by App")
}
@Test
fun testPhoneCall() {
val element = PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
false,
false,
true,
TEST_PERM_GROUP,
null
)
val list = listOf(createPrivacyElement(isPhoneCall = true))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
val list = listOf(element)
dialog = PrivacyDialogV2(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
context.getString(R.string.ongoing_privacy_dialog_phonecall)
)
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("Recently used in phone call")
}
@Test
fun testPhoneCallNotClickable() {
val element = PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
false,
false,
true,
TEST_PERM_GROUP,
null
)
val list = listOf(createPrivacyElement(isPhoneCall = true))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
val list = listOf(element)
dialog = PrivacyDialogV2(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<View>(R.id.privacy_item).isClickable).isFalse()
assertThat(dialog.requireViewById<View>(R.id.chevron).visibility).isEqualTo(View.GONE)
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 element = PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
"proxyLabel",
0L,
false,
false,
true,
TEST_PERM_GROUP,
null
)
val list = listOf(createPrivacyElement(proxyLabel = "proxy label"))
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
val list = listOf(element)
dialog = PrivacyDialogV2(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
context.getString(
R.string.ongoing_privacy_dialog_attribution_text,
element.proxyLabel
)
)
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
.isEqualTo("Recently used by App (proxy label)")
}
@Test
fun testSubattribution() {
val element = PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
"For subattribution",
null,
0L,
true,
false,
false,
TEST_PERM_GROUP,
null
)
val list = listOf(element)
dialog = PrivacyDialogV2(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
context.getString(
R.string.ongoing_privacy_dialog_attribution_label,
element.attributionLabel
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 element = PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
"For subattribution",
"proxy label",
0L,
true,
false,
false,
TEST_PERM_GROUP,
null
)
val list = listOf(element)
dialog = PrivacyDialogV2(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
context.getString(
R.string.ongoing_privacy_dialog_attribution_proxy_label,
element.attributionLabel, element.proxyLabel
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() {
// Dialog must have a non-empty title for a11y purposes.
val list = listOf(
PrivacyDialogV2.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
TEST_PACKAGE_NAME,
TEST_USER_ID,
"App",
null,
null,
null,
0L,
false,
false,
false,
TEST_PERM_GROUP,
null
)
)
dialog = PrivacyDialogV2(context, list, starter)
val list = listOf(createPrivacyElement())
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
dialog.show()
assertThat(TextUtils.isEmpty(dialog.window?.attributes?.title)).isFalse()
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

@@ -27,6 +27,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
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.time.FakeSystemClock
import org.junit.Before
@@ -171,7 +172,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never()).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
.showDialog(any(Context::class.java), any(View::class.java))
}
@Test
@@ -183,7 +185,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never()).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
.showDialog(any(Context::class.java), any(View::class.java))
}
@Test
@@ -203,7 +206,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never()).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
.showDialog(any(Context::class.java), any(View::class.java))
}
@Test
@@ -222,7 +226,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogControllerV2).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2).showDialog(any(Context::class.java), eq(privacyChip))
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
}