New privacy dialog
go/new-sysui-privacy-dialog Bug: 283456173 Test: atest SystemUITests Change-Id: I63e6200e418fff94acc8dd7eaa551d15e049ba8a
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
25
packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml
Normal file
25
packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
26
packages/SystemUI/res/layout/privacy_dialog_card_button.xml
Normal file
26
packages/SystemUI/res/layout/privacy_dialog_card_button.xml
Normal 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"/>
|
||||
89
packages/SystemUI/res/layout/privacy_dialog_item_v2.xml
Normal file
89
packages/SystemUI/res/layout/privacy_dialog_item_v2.xml
Normal 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>
|
||||
109
packages/SystemUI/res/layout/privacy_dialog_v2.xml
Normal file
109
packages/SystemUI/res/layout/privacy_dialog_v2.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user