Merge changes from topic "scPermDialog" into udc-qpr-dev am: afb8816434
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23262861 Change-Id: Iea1c064580ded620c14db273e3ab82a8c317e5e7 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -32,11 +32,10 @@ import android.view.ViewRootImpl
|
|||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
|
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
|
||||||
import android.widget.FrameLayout
|
|
||||||
import com.android.app.animation.Interpolators
|
import com.android.app.animation.Interpolators
|
||||||
import com.android.internal.jank.InteractionJankMonitor
|
import com.android.internal.jank.InteractionJankMonitor
|
||||||
import com.android.internal.jank.InteractionJankMonitor.CujType
|
import com.android.internal.jank.InteractionJankMonitor.CujType
|
||||||
import com.android.systemui.animation.view.LaunchableFrameLayout
|
import com.android.systemui.util.maybeForceFullscreen
|
||||||
import com.android.systemui.util.registerAnimationOnBackInvoked
|
import com.android.systemui.util.registerAnimationOnBackInvoked
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -622,96 +621,12 @@ private class AnimatedDialog(
|
|||||||
|
|
||||||
viewGroupWithBackground
|
viewGroupWithBackground
|
||||||
} else {
|
} else {
|
||||||
// We will make the dialog window (and therefore its DecorView) fullscreen to make
|
val (dialogContentWithBackground, decorViewLayoutListener) =
|
||||||
// it possible to animate outside its bounds.
|
dialog.maybeForceFullscreen()!!
|
||||||
//
|
this.decorViewLayoutListener = decorViewLayoutListener
|
||||||
// Before that, we add a new View as a child of the DecorView with the same size and
|
|
||||||
// gravity as that DecorView, then we add all original children of the DecorView to
|
|
||||||
// that new View. Finally we remove the background of the DecorView and add it to
|
|
||||||
// the new View, then we make the DecorView fullscreen. This new View now acts as a
|
|
||||||
// fake (non fullscreen) window.
|
|
||||||
//
|
|
||||||
// On top of that, we also add a fullscreen transparent background between the
|
|
||||||
// DecorView and the view that we added so that we can dismiss the dialog when this
|
|
||||||
// view is clicked. This is necessary because DecorView overrides onTouchEvent and
|
|
||||||
// therefore we can't set the click listener directly on the (now fullscreen)
|
|
||||||
// DecorView.
|
|
||||||
val fullscreenTransparentBackground = FrameLayout(dialog.context)
|
|
||||||
decorView.addView(
|
|
||||||
fullscreenTransparentBackground,
|
|
||||||
0 /* index */,
|
|
||||||
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
|
||||||
)
|
|
||||||
|
|
||||||
val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
|
|
||||||
dialogContentWithBackground.background = decorView.background
|
|
||||||
|
|
||||||
// Make the window background transparent. Note that setting the window (or
|
|
||||||
// DecorView) background drawable to null leads to issues with background color (not
|
|
||||||
// being transparent) or with insets that are not refreshed. Therefore we need to
|
|
||||||
// set it to something not null, hence we are using android.R.color.transparent
|
|
||||||
// here.
|
|
||||||
window.setBackgroundDrawableResource(android.R.color.transparent)
|
|
||||||
|
|
||||||
// Close the dialog when clicking outside of it.
|
|
||||||
fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
|
|
||||||
dialogContentWithBackground.isClickable = true
|
|
||||||
|
|
||||||
// Make sure the transparent and dialog backgrounds are not focusable by
|
|
||||||
// accessibility
|
|
||||||
// features.
|
|
||||||
fullscreenTransparentBackground.importantForAccessibility =
|
|
||||||
View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
||||||
dialogContentWithBackground.importantForAccessibility =
|
|
||||||
View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
||||||
|
|
||||||
fullscreenTransparentBackground.addView(
|
|
||||||
dialogContentWithBackground,
|
|
||||||
FrameLayout.LayoutParams(
|
|
||||||
window.attributes.width,
|
|
||||||
window.attributes.height,
|
|
||||||
window.attributes.gravity
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Move all original children of the DecorView to the new View we just added.
|
|
||||||
for (i in 1 until decorView.childCount) {
|
|
||||||
val view = decorView.getChildAt(1)
|
|
||||||
decorView.removeViewAt(1)
|
|
||||||
dialogContentWithBackground.addView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the window fullscreen and add a layout listener to ensure it stays
|
|
||||||
// fullscreen.
|
|
||||||
window.setLayout(MATCH_PARENT, MATCH_PARENT)
|
|
||||||
decorViewLayoutListener =
|
|
||||||
View.OnLayoutChangeListener {
|
|
||||||
v,
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
right,
|
|
||||||
bottom,
|
|
||||||
oldLeft,
|
|
||||||
oldTop,
|
|
||||||
oldRight,
|
|
||||||
oldBottom ->
|
|
||||||
if (
|
|
||||||
window.attributes.width != MATCH_PARENT ||
|
|
||||||
window.attributes.height != MATCH_PARENT
|
|
||||||
) {
|
|
||||||
// The dialog size changed, copy its size to dialogContentWithBackground
|
|
||||||
// and make the dialog window full screen again.
|
|
||||||
val layoutParams = dialogContentWithBackground.layoutParams
|
|
||||||
layoutParams.width = window.attributes.width
|
|
||||||
layoutParams.height = window.attributes.height
|
|
||||||
dialogContentWithBackground.layoutParams = layoutParams
|
|
||||||
window.setLayout(MATCH_PARENT, MATCH_PARENT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decorView.addOnLayoutChangeListener(decorViewLayoutListener)
|
|
||||||
|
|
||||||
dialogContentWithBackground
|
dialogContentWithBackground
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialogContentWithBackground = dialogContentWithBackground
|
this.dialogContentWithBackground = dialogContentWithBackground
|
||||||
dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
|
dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ package com.android.systemui.util
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.window.OnBackInvokedDispatcher
|
import android.window.OnBackInvokedDispatcher
|
||||||
import com.android.systemui.animation.back.BackAnimationSpec
|
import com.android.systemui.animation.back.BackAnimationSpec
|
||||||
import com.android.systemui.animation.back.BackTransformation
|
import com.android.systemui.animation.back.BackTransformation
|
||||||
@@ -25,6 +28,7 @@ import com.android.systemui.animation.back.applyTo
|
|||||||
import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
|
import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
|
||||||
import com.android.systemui.animation.back.onBackAnimationCallbackFrom
|
import com.android.systemui.animation.back.onBackAnimationCallbackFrom
|
||||||
import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached
|
import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached
|
||||||
|
import com.android.systemui.animation.view.LaunchableFrameLayout
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec].
|
* Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec].
|
||||||
@@ -49,3 +53,110 @@ fun Dialog.registerAnimationOnBackInvoked(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate
|
||||||
|
* outside its bounds. No-op if the dialog is already fullscreen.
|
||||||
|
*
|
||||||
|
* <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view
|
||||||
|
* and a layout listener. The new view matches the original dialog DecorView in size, position, and
|
||||||
|
* background. This new view will be a child of the modified, transparent, fullscreen DecorView. The
|
||||||
|
* layout listener is listening to changes to the modified DecorView. It is the responsibility of
|
||||||
|
* the caller to deregister the listener when the dialog is dismissed.
|
||||||
|
*/
|
||||||
|
fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? {
|
||||||
|
// Create the dialog so that its onCreate() method is called, which usually sets the dialog
|
||||||
|
// content.
|
||||||
|
create()
|
||||||
|
|
||||||
|
val window = window!!
|
||||||
|
val decorView = window.decorView as ViewGroup
|
||||||
|
|
||||||
|
val isWindowFullscreen =
|
||||||
|
window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
|
||||||
|
if (isWindowFullscreen) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will make the dialog window (and therefore its DecorView) fullscreen to make it possible
|
||||||
|
// to animate outside its bounds.
|
||||||
|
//
|
||||||
|
// Before that, we add a new View as a child of the DecorView with the same size and gravity as
|
||||||
|
// that DecorView, then we add all original children of the DecorView to that new View. Finally
|
||||||
|
// we remove the background of the DecorView and add it to the new View, then we make the
|
||||||
|
// DecorView fullscreen. This new View now acts as a fake (non fullscreen) window.
|
||||||
|
//
|
||||||
|
// On top of that, we also add a fullscreen transparent background between the DecorView and the
|
||||||
|
// view that we added so that we can dismiss the dialog when this view is clicked. This is
|
||||||
|
// necessary because DecorView overrides onTouchEvent and therefore we can't set the click
|
||||||
|
// listener directly on the (now fullscreen) DecorView.
|
||||||
|
val fullscreenTransparentBackground = FrameLayout(context)
|
||||||
|
decorView.addView(
|
||||||
|
fullscreenTransparentBackground,
|
||||||
|
0 /* index */,
|
||||||
|
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
|
)
|
||||||
|
|
||||||
|
val dialogContentWithBackground = LaunchableFrameLayout(context)
|
||||||
|
dialogContentWithBackground.background = decorView.background
|
||||||
|
|
||||||
|
// Make the window background transparent. Note that setting the window (or DecorView)
|
||||||
|
// background drawable to null leads to issues with background color (not being transparent) or
|
||||||
|
// with insets that are not refreshed. Therefore we need to set it to something not null, hence
|
||||||
|
// we are using android.R.color.transparent here.
|
||||||
|
window.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
|
||||||
|
// Close the dialog when clicking outside of it.
|
||||||
|
fullscreenTransparentBackground.setOnClickListener { dismiss() }
|
||||||
|
dialogContentWithBackground.isClickable = true
|
||||||
|
|
||||||
|
// Make sure the transparent and dialog backgrounds are not focusable by accessibility
|
||||||
|
// features.
|
||||||
|
fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
|
||||||
|
fullscreenTransparentBackground.addView(
|
||||||
|
dialogContentWithBackground,
|
||||||
|
FrameLayout.LayoutParams(
|
||||||
|
window.attributes.width,
|
||||||
|
window.attributes.height,
|
||||||
|
window.attributes.gravity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Move all original children of the DecorView to the new View we just added.
|
||||||
|
for (i in 1 until decorView.childCount) {
|
||||||
|
val view = decorView.getChildAt(1)
|
||||||
|
decorView.removeViewAt(1)
|
||||||
|
dialogContentWithBackground.addView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the window fullscreen and add a layout listener to ensure it stays fullscreen.
|
||||||
|
window.setLayout(MATCH_PARENT, MATCH_PARENT)
|
||||||
|
val decorViewLayoutListener =
|
||||||
|
View.OnLayoutChangeListener {
|
||||||
|
v,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
oldLeft,
|
||||||
|
oldTop,
|
||||||
|
oldRight,
|
||||||
|
oldBottom ->
|
||||||
|
if (
|
||||||
|
window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT
|
||||||
|
) {
|
||||||
|
// The dialog size changed, copy its size to dialogContentWithBackground and make
|
||||||
|
// the dialog window full screen again.
|
||||||
|
val layoutParams = dialogContentWithBackground.layoutParams
|
||||||
|
layoutParams.width = window.attributes.width
|
||||||
|
layoutParams.height = window.attributes.height
|
||||||
|
dialogContentWithBackground.layoutParams = layoutParams
|
||||||
|
window.setLayout(MATCH_PARENT, MATCH_PARENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decorView.addOnLayoutChangeListener(decorViewLayoutListener)
|
||||||
|
|
||||||
|
return dialogContentWithBackground to decorViewLayoutListener
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
-->
|
-->
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
<resources xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
|
||||||
<drawable name="notification_number_text_color">#ffffffff</drawable>
|
<drawable name="notification_number_text_color">#ffffffff</drawable>
|
||||||
<drawable name="system_bar_background">@color/system_bar_background_opaque</drawable>
|
<drawable name="system_bar_background">@color/system_bar_background_opaque</drawable>
|
||||||
<color name="system_bar_background_opaque">#ff000000</color>
|
<color name="system_bar_background_opaque">#ff000000</color>
|
||||||
|
|||||||
@@ -1097,6 +1097,9 @@
|
|||||||
|
|
||||||
<dimen name="ongoing_appops_dialog_side_padding">16dp</dimen>
|
<dimen name="ongoing_appops_dialog_side_padding">16dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="privacy_dialog_background_radius_large">12dp</dimen>
|
||||||
|
<dimen name="privacy_dialog_background_radius_small">4dp</dimen>
|
||||||
|
|
||||||
<!-- Size of media cards in the QSPanel carousel -->
|
<!-- Size of media cards in the QSPanel carousel -->
|
||||||
<dimen name="qs_media_padding">16dp</dimen>
|
<dimen name="qs_media_padding">16dp</dimen>
|
||||||
<dimen name="qs_media_album_radius">14dp</dimen>
|
<dimen name="qs_media_album_radius">14dp</dimen>
|
||||||
|
|||||||
@@ -214,4 +214,8 @@
|
|||||||
<item type="id" name="nssl_guideline" />
|
<item type="id" name="nssl_guideline" />
|
||||||
<item type="id" name="lock_icon" />
|
<item type="id" name="lock_icon" />
|
||||||
<item type="id" name="lock_icon_bg" />
|
<item type="id" name="lock_icon_bg" />
|
||||||
|
|
||||||
|
<!-- Privacy dialog -->
|
||||||
|
<item type="id" name="privacy_dialog_close_app_button" />
|
||||||
|
<item type="id" name="privacy_dialog_manage_app_button" />
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -3170,10 +3170,43 @@
|
|||||||
<!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] -->
|
<!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] -->
|
||||||
<string name="set_default_notes_app_toast_content">Set default notes app in Settings</string>
|
<string name="set_default_notes_app_toast_content">Set default notes app in Settings</string>
|
||||||
|
|
||||||
<!--
|
<!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. -->
|
||||||
Label for a button that, when clicked, sends the user to the app store to install an app.
|
|
||||||
|
|
||||||
[CHAR LIMIT=64].
|
|
||||||
-->
|
|
||||||
<string name="install_app">Install app</string>
|
<string name="install_app">Install app</string>
|
||||||
|
|
||||||
|
<!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] -->
|
||||||
|
<string name="privacy_dialog_title">Microphone & Camera</string>
|
||||||
|
<!-- Subtitle of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_summary">Recent app use</string>
|
||||||
|
<!-- Label of the secondary button of the privacy dialog, used to check recent app usage of phone sensors [CHAR LIMIT=30] -->
|
||||||
|
<string name="privacy_dialog_more_button">See recent access</string>
|
||||||
|
<!-- Label of the primary button to dismiss the privacy dialog [CHAR LIMIT=20] -->
|
||||||
|
<string name="privacy_dialog_done_button">Done</string>
|
||||||
|
<!-- Description for expanding a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_expand_action">Expand and show options</string>
|
||||||
|
<!-- Description for collapsing a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_collapse_action">Collapse</string>
|
||||||
|
<!-- Label of a button of the privacy dialog to close an app actively using a phone sensor [CHAR LIMIT=50] -->
|
||||||
|
<string name="privacy_dialog_close_app_button">Close this app</string>
|
||||||
|
<!-- Message shown in the privacy dialog when an app actively using a phone sensor is closed [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_close_app_message"><xliff:g id="app_name" example="Gmail">%1$s</xliff:g> closed</string>
|
||||||
|
<!-- Label of a button of the privacy dialog to learn more of a service actively or recently using a phone sensor [CHAR LIMIT=50] -->
|
||||||
|
<string name="privacy_dialog_manage_service">Manage service</string>
|
||||||
|
<!-- Label of a button of the privacy dialog to manage permissions of an app actively or recently using a phone sensor [CHAR LIMIT=50] -->
|
||||||
|
<string name="privacy_dialog_manage_permissions">Manage access</string>
|
||||||
|
<!-- Label for active usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_active_call_usage">In use by phone call</string>
|
||||||
|
<!-- Label for recent usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_recent_call_usage">Recently used in phone call</string>
|
||||||
|
<!-- Label for active app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_active_app_usage">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
|
||||||
|
<!-- Label for recent app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_recent_app_usage">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
|
||||||
|
<!-- Label for active app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_active_app_usage_1">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
|
||||||
|
<!-- Label for recent app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_recent_app_usage_1">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
|
||||||
|
<!-- Label for active app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
|
||||||
|
<!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
|
||||||
|
<string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1439,4 +1439,22 @@
|
|||||||
<item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item>
|
<item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item>
|
||||||
<item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item>
|
<item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.PrivacyDialog.Item.Title"
|
||||||
|
parent="@android:style/TextAppearance.DeviceDefault.Medium">
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
<item name="android:lineHeight">20sp</item>
|
||||||
|
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.PrivacyDialog.Item.Summary"
|
||||||
|
parent="@android:style/TextAppearance.DeviceDefault.Small">
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
<item name="android:lineHeight">20sp</item>
|
||||||
|
<item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.PrivacyDialog" parent="@style/Theme.SystemUI.Dialog">
|
||||||
|
<item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainer</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,367 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.privacy
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.UserHandle
|
||||||
|
import android.permission.PermissionGroupUsage
|
||||||
|
import android.permission.PermissionManager
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.android.internal.logging.UiEventLogger
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator
|
||||||
|
import com.android.systemui.appops.AppOpsController
|
||||||
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
|
import com.android.systemui.dagger.qualifiers.Background
|
||||||
|
import com.android.systemui.dagger.qualifiers.Main
|
||||||
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
|
import com.android.systemui.privacy.logging.PrivacyLogger
|
||||||
|
import com.android.systemui.settings.UserTracker
|
||||||
|
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val defaultDialogProvider =
|
||||||
|
object : PrivacyDialogControllerV2.DialogProvider {
|
||||||
|
override fun makeDialog(
|
||||||
|
context: Context,
|
||||||
|
list: List<PrivacyDialogV2.PrivacyElement>,
|
||||||
|
manageApp: (String, Int, Intent) -> Unit,
|
||||||
|
closeApp: (String, Int) -> Unit,
|
||||||
|
openPrivacyDashboard: () -> Unit
|
||||||
|
): PrivacyDialogV2 {
|
||||||
|
return PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for [PrivacyDialogV2].
|
||||||
|
*
|
||||||
|
* This controller shows and dismissed the dialog, as well as determining the information to show in
|
||||||
|
* it.
|
||||||
|
*/
|
||||||
|
@SysUISingleton
|
||||||
|
class PrivacyDialogControllerV2(
|
||||||
|
private val permissionManager: PermissionManager,
|
||||||
|
private val packageManager: PackageManager,
|
||||||
|
private val privacyItemController: PrivacyItemController,
|
||||||
|
private val userTracker: UserTracker,
|
||||||
|
private val activityStarter: ActivityStarter,
|
||||||
|
private val backgroundExecutor: Executor,
|
||||||
|
private val uiExecutor: Executor,
|
||||||
|
private val privacyLogger: PrivacyLogger,
|
||||||
|
private val keyguardStateController: KeyguardStateController,
|
||||||
|
private val appOpsController: AppOpsController,
|
||||||
|
private val uiEventLogger: UiEventLogger,
|
||||||
|
private val dialogLaunchAnimator: DialogLaunchAnimator,
|
||||||
|
private val dialogProvider: DialogProvider
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
permissionManager: PermissionManager,
|
||||||
|
packageManager: PackageManager,
|
||||||
|
privacyItemController: PrivacyItemController,
|
||||||
|
userTracker: UserTracker,
|
||||||
|
activityStarter: ActivityStarter,
|
||||||
|
@Background backgroundExecutor: Executor,
|
||||||
|
@Main uiExecutor: Executor,
|
||||||
|
privacyLogger: PrivacyLogger,
|
||||||
|
keyguardStateController: KeyguardStateController,
|
||||||
|
appOpsController: AppOpsController,
|
||||||
|
uiEventLogger: UiEventLogger,
|
||||||
|
dialogLaunchAnimator: DialogLaunchAnimator
|
||||||
|
) : this(
|
||||||
|
permissionManager,
|
||||||
|
packageManager,
|
||||||
|
privacyItemController,
|
||||||
|
userTracker,
|
||||||
|
activityStarter,
|
||||||
|
backgroundExecutor,
|
||||||
|
uiExecutor,
|
||||||
|
privacyLogger,
|
||||||
|
keyguardStateController,
|
||||||
|
appOpsController,
|
||||||
|
uiEventLogger,
|
||||||
|
dialogLaunchAnimator,
|
||||||
|
defaultDialogProvider
|
||||||
|
)
|
||||||
|
|
||||||
|
private var dialog: Dialog? = null
|
||||||
|
|
||||||
|
private val onDialogDismissed =
|
||||||
|
object : PrivacyDialogV2.OnDialogDismissed {
|
||||||
|
override fun onDialogDismissed() {
|
||||||
|
privacyLogger.logPrivacyDialogDismissed()
|
||||||
|
uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
|
||||||
|
dialog = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun closeApp(packageName: String, userId: Int) {
|
||||||
|
uiEventLogger.log(
|
||||||
|
PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
|
||||||
|
userId,
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
privacyLogger.logCloseAppFromDialog(packageName, userId)
|
||||||
|
ActivityManager.getService().stopAppForUser(packageName, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
private fun manageApp(packageName: String, userId: Int, navigationIntent: Intent) {
|
||||||
|
uiEventLogger.log(
|
||||||
|
PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
|
||||||
|
userId,
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
privacyLogger.logStartSettingsActivityFromDialog(packageName, userId)
|
||||||
|
startActivity(navigationIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
private fun openPrivacyDashboard() {
|
||||||
|
uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
|
||||||
|
privacyLogger.logStartPrivacyDashboardFromDialog()
|
||||||
|
startActivity(Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE))
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
private fun startActivity(navigationIntent: Intent) {
|
||||||
|
if (!keyguardStateController.isUnlocked) {
|
||||||
|
// If we are locked, hide the dialog so the user can unlock
|
||||||
|
dialog?.hide()
|
||||||
|
}
|
||||||
|
// startActivity calls internally startActivityDismissingKeyguard
|
||||||
|
activityStarter.startActivity(navigationIntent, true) {
|
||||||
|
if (ActivityManager.isStartResultSuccessful(it)) {
|
||||||
|
dismissDialog()
|
||||||
|
} else {
|
||||||
|
dialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun getStartViewPermissionUsageIntent(
|
||||||
|
packageName: String,
|
||||||
|
permGroupName: String,
|
||||||
|
attributionTag: CharSequence?,
|
||||||
|
isAttributionSupported: Boolean
|
||||||
|
): Intent? {
|
||||||
|
if (attributionTag != null && isAttributionSupported) {
|
||||||
|
val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE)
|
||||||
|
intent.setPackage(packageName)
|
||||||
|
intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName)
|
||||||
|
intent.putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, arrayOf(attributionTag.toString()))
|
||||||
|
intent.putExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, true)
|
||||||
|
val resolveInfo =
|
||||||
|
packageManager.resolveActivity(intent, PackageManager.ResolveInfoFlags.of(0))
|
||||||
|
if (
|
||||||
|
resolveInfo?.activityInfo?.permission ==
|
||||||
|
Manifest.permission.START_VIEW_PERMISSION_USAGE
|
||||||
|
) {
|
||||||
|
intent.component = ComponentName(packageName, resolveInfo.activityInfo.name)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent {
|
||||||
|
val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS)
|
||||||
|
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
|
||||||
|
intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId))
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun permGroupUsage(): List<PermissionGroupUsage> {
|
||||||
|
return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the [PrivacyDialogV2]
|
||||||
|
*
|
||||||
|
* This retrieves the permission usage from [PermissionManager] and creates a new
|
||||||
|
* [PrivacyDialogV2] with a list of [PrivacyDialogV2.PrivacyElement] to show.
|
||||||
|
*
|
||||||
|
* This list will be filtered by [filterAndSelect]. Only types available by
|
||||||
|
* [PrivacyItemController] will be shown.
|
||||||
|
*
|
||||||
|
* @param context A context to use to create the dialog.
|
||||||
|
* @see filterAndSelect
|
||||||
|
*/
|
||||||
|
fun showDialog(context: Context, view: View? = null) {
|
||||||
|
dismissDialog()
|
||||||
|
backgroundExecutor.execute {
|
||||||
|
val usage = permGroupUsage()
|
||||||
|
val userInfos = userTracker.userProfiles
|
||||||
|
privacyLogger.logUnfilteredPermGroupUsage(usage)
|
||||||
|
val items =
|
||||||
|
usage.mapNotNull {
|
||||||
|
val userInfo =
|
||||||
|
userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
|
||||||
|
if (
|
||||||
|
isAvailable(it.permissionGroupName) && (userInfo != null || it.isPhoneCall)
|
||||||
|
) {
|
||||||
|
// Only try to get the app name if we actually need it
|
||||||
|
val appName =
|
||||||
|
if (it.isPhoneCall) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
getLabelForPackage(it.packageName, it.uid)
|
||||||
|
}
|
||||||
|
val userId = UserHandle.getUserId(it.uid)
|
||||||
|
val viewUsageIntent =
|
||||||
|
getStartViewPermissionUsageIntent(
|
||||||
|
it.packageName,
|
||||||
|
it.permissionGroupName,
|
||||||
|
it.attributionTag,
|
||||||
|
// attributionLabel is set only when subattribution policies
|
||||||
|
// are supported and satisfied
|
||||||
|
it.attributionLabel != null
|
||||||
|
)
|
||||||
|
PrivacyDialogV2.PrivacyElement(
|
||||||
|
permGroupToPrivacyType(it.permissionGroupName)!!,
|
||||||
|
it.packageName,
|
||||||
|
userId,
|
||||||
|
appName,
|
||||||
|
it.attributionTag,
|
||||||
|
it.attributionLabel,
|
||||||
|
it.proxyLabel,
|
||||||
|
it.lastAccessTimeMillis,
|
||||||
|
it.isActive,
|
||||||
|
it.isPhoneCall,
|
||||||
|
viewUsageIntent != null,
|
||||||
|
it.permissionGroupName,
|
||||||
|
viewUsageIntent
|
||||||
|
?: getDefaultManageAppPermissionsIntent(it.packageName, userId)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiExecutor.execute {
|
||||||
|
val elements = filterAndSelect(items)
|
||||||
|
if (elements.isNotEmpty()) {
|
||||||
|
val d =
|
||||||
|
dialogProvider.makeDialog(
|
||||||
|
context,
|
||||||
|
elements,
|
||||||
|
this::manageApp,
|
||||||
|
this::closeApp,
|
||||||
|
this::openPrivacyDashboard
|
||||||
|
)
|
||||||
|
d.setShowForAllUsers(true)
|
||||||
|
d.addOnDismissListener(onDialogDismissed)
|
||||||
|
if (view != null) {
|
||||||
|
dialogLaunchAnimator.showFromView(d, view)
|
||||||
|
} else {
|
||||||
|
d.show()
|
||||||
|
}
|
||||||
|
privacyLogger.logShowDialogV2Contents(elements)
|
||||||
|
dialog = d
|
||||||
|
} else {
|
||||||
|
privacyLogger.logEmptyDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dismisses the dialog */
|
||||||
|
fun dismissDialog() {
|
||||||
|
dialog?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun getLabelForPackage(packageName: String, uid: Int): CharSequence {
|
||||||
|
return try {
|
||||||
|
packageManager
|
||||||
|
.getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid))
|
||||||
|
.loadLabel(packageManager)
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
privacyLogger.logLabelNotFound(packageName)
|
||||||
|
packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun permGroupToPrivacyType(group: String): PrivacyType? {
|
||||||
|
return when (group) {
|
||||||
|
Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA
|
||||||
|
Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE
|
||||||
|
Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAvailable(group: String): Boolean {
|
||||||
|
return when (group) {
|
||||||
|
Manifest.permission_group.CAMERA -> privacyItemController.micCameraAvailable
|
||||||
|
Manifest.permission_group.MICROPHONE -> privacyItemController.micCameraAvailable
|
||||||
|
Manifest.permission_group.LOCATION -> privacyItemController.locationAvailable
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the list of elements to show.
|
||||||
|
*
|
||||||
|
* For each privacy type, it'll return all active elements. If there are no active elements,
|
||||||
|
* it'll return the most recent access
|
||||||
|
*/
|
||||||
|
private fun filterAndSelect(
|
||||||
|
list: List<PrivacyDialogV2.PrivacyElement>
|
||||||
|
): List<PrivacyDialogV2.PrivacyElement> {
|
||||||
|
return list
|
||||||
|
.groupBy { it.type }
|
||||||
|
.toSortedMap()
|
||||||
|
.flatMap { (_, elements) ->
|
||||||
|
val actives = elements.filter { it.isActive }
|
||||||
|
if (actives.isNotEmpty()) {
|
||||||
|
actives.sortedByDescending { it.lastActiveTimestamp }
|
||||||
|
} else {
|
||||||
|
elements.maxByOrNull { it.lastActiveTimestamp }?.let { listOf(it) }
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to create a [PrivacyDialogV2].
|
||||||
|
*
|
||||||
|
* Can be used to inject a mock creator.
|
||||||
|
*/
|
||||||
|
interface DialogProvider {
|
||||||
|
/** Create a [PrivacyDialogV2]. */
|
||||||
|
fun makeDialog(
|
||||||
|
context: Context,
|
||||||
|
list: List<PrivacyDialogV2.PrivacyElement>,
|
||||||
|
manageApp: (String, Int, Intent) -> Unit,
|
||||||
|
closeApp: (String, Int) -> Unit,
|
||||||
|
openPrivacyDashboard: () -> Unit
|
||||||
|
): PrivacyDialogV2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,13 +18,20 @@ package com.android.systemui.privacy
|
|||||||
|
|
||||||
import com.android.internal.logging.UiEvent
|
import com.android.internal.logging.UiEvent
|
||||||
import com.android.internal.logging.UiEventLogger
|
import com.android.internal.logging.UiEventLogger
|
||||||
|
import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
|
||||||
|
|
||||||
enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
|
enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
|
||||||
@UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.")
|
@UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.")
|
||||||
PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904),
|
PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904),
|
||||||
|
|
||||||
@UiEvent(doc = "Privacy dialog is dismissed by user.")
|
@UiEvent(doc = "Privacy dialog is dismissed by user.")
|
||||||
PRIVACY_DIALOG_DISMISSED(905);
|
PRIVACY_DIALOG_DISMISSED(905),
|
||||||
|
|
||||||
|
@UiEvent(doc = "Privacy dialog item is clicked by user to close the app using a sensor.")
|
||||||
|
PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP(1396),
|
||||||
|
|
||||||
|
@UiEvent(doc = "Privacy dialog is clicked by user to see the privacy dashboard.")
|
||||||
|
PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD(1397);
|
||||||
|
|
||||||
override fun getId() = _id
|
override fun getId() = _id
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.privacy
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageItemInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
import android.content.res.Resources.NotFoundException
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
|
||||||
|
import com.android.settingslib.Utils
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.animation.ViewHierarchyAnimator
|
||||||
|
import com.android.systemui.statusbar.phone.SystemUIDialog
|
||||||
|
import com.android.systemui.util.maybeForceFullscreen
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog to show ongoing and recent app ops element.
|
||||||
|
*
|
||||||
|
* @param context A context to create the dialog
|
||||||
|
* @param list list of elements to show in the dialog. The elements will show in the same order they
|
||||||
|
* appear in the list
|
||||||
|
* @param manageApp a callback to start an activity for a given package name, user id, and intent
|
||||||
|
* @param closeApp a callback to close an app for a given package name, user id
|
||||||
|
* @param openPrivacyDashboard a callback to open the privacy dashboard
|
||||||
|
* @see PrivacyDialogControllerV2
|
||||||
|
*/
|
||||||
|
class PrivacyDialogV2(
|
||||||
|
context: Context,
|
||||||
|
private val list: List<PrivacyElement>,
|
||||||
|
private val manageApp: (String, Int, Intent) -> Unit,
|
||||||
|
private val closeApp: (String, Int) -> Unit,
|
||||||
|
private val openPrivacyDashboard: () -> Unit
|
||||||
|
) : SystemUIDialog(context, R.style.Theme_PrivacyDialog) {
|
||||||
|
|
||||||
|
private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
|
||||||
|
private val dismissed = AtomicBoolean(false)
|
||||||
|
// Note: this will call the dialog create method during init
|
||||||
|
private val decorViewLayoutListener = maybeForceFullscreen()?.component2()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a listener that will be called when the dialog is dismissed.
|
||||||
|
*
|
||||||
|
* If the dialog has already been dismissed, the listener will be called immediately, in the
|
||||||
|
* same thread.
|
||||||
|
*/
|
||||||
|
fun addOnDismissListener(listener: OnDialogDismissed) {
|
||||||
|
if (dismissed.get()) {
|
||||||
|
listener.onDialogDismissed()
|
||||||
|
} else {
|
||||||
|
dismissListeners.add(WeakReference(listener))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
dismissed.set(true)
|
||||||
|
val iterator = dismissListeners.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val el = iterator.next()
|
||||||
|
iterator.remove()
|
||||||
|
el.get()?.onDialogDismissed()
|
||||||
|
}
|
||||||
|
// Remove the layout change listener we may have added to the DecorView.
|
||||||
|
if (decorViewLayoutListener != null) {
|
||||||
|
window!!.decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
window!!.setGravity(Gravity.CENTER)
|
||||||
|
setTitle(R.string.privacy_dialog_title)
|
||||||
|
setContentView(R.layout.privacy_dialog_v2)
|
||||||
|
|
||||||
|
val closeButton = requireViewById<Button>(R.id.privacy_dialog_close_button)
|
||||||
|
closeButton.setOnClickListener { dismiss() }
|
||||||
|
|
||||||
|
val moreButton = requireViewById<Button>(R.id.privacy_dialog_more_button)
|
||||||
|
moreButton.setOnClickListener { openPrivacyDashboard() }
|
||||||
|
|
||||||
|
val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
|
||||||
|
list.forEach { itemsContainer.addView(createView(it, itemsContainer)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createView(element: PrivacyElement, itemsContainer: ViewGroup): View {
|
||||||
|
val itemCard =
|
||||||
|
LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.privacy_dialog_item_v2, itemsContainer, false) as ViewGroup
|
||||||
|
|
||||||
|
updateItemHeader(element, itemCard)
|
||||||
|
|
||||||
|
if (element.isPhoneCall) {
|
||||||
|
return itemCard
|
||||||
|
}
|
||||||
|
|
||||||
|
setItemExpansionBehavior(itemCard)
|
||||||
|
|
||||||
|
configureIndicatorActionButtons(element, itemCard)
|
||||||
|
|
||||||
|
return itemCard
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateItemHeader(element: PrivacyElement, itemCard: View) {
|
||||||
|
val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
|
||||||
|
val permGroupLabel = context.packageManager.getDefaultPermGroupLabel(element.permGroupName)
|
||||||
|
|
||||||
|
val iconView = itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
|
||||||
|
val indicatorIcon = context.getPermGroupIcon(element.permGroupName)
|
||||||
|
updateIconView(iconView, indicatorIcon, element.isActive)
|
||||||
|
iconView.contentDescription = permGroupLabel
|
||||||
|
|
||||||
|
val titleView = itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_title)!!
|
||||||
|
titleView.text = permGroupLabel
|
||||||
|
titleView.contentDescription = permGroupLabel
|
||||||
|
|
||||||
|
val usageText = getUsageText(element)
|
||||||
|
val summaryView =
|
||||||
|
itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
|
||||||
|
summaryView.text = usageText
|
||||||
|
summaryView.contentDescription = usageText
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureIndicatorActionButtons(element: PrivacyElement, itemCard: View) {
|
||||||
|
val expandedLayout =
|
||||||
|
itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header_expanded_layout)!!
|
||||||
|
|
||||||
|
val buttons: MutableList<View> = mutableListOf()
|
||||||
|
configureCloseAppButton(element, expandedLayout)?.also { buttons.add(it) }
|
||||||
|
buttons.add(configureManageButton(element, expandedLayout))
|
||||||
|
|
||||||
|
val backgroundColor = getBackgroundColor(element.isActive)
|
||||||
|
when (buttons.size) {
|
||||||
|
0 -> return
|
||||||
|
1 -> {
|
||||||
|
val background =
|
||||||
|
getMutableDrawable(R.drawable.privacy_dialog_background_large_top_large_bottom)
|
||||||
|
background.setTint(backgroundColor)
|
||||||
|
buttons[0].background = background
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val firstBackground =
|
||||||
|
getMutableDrawable(R.drawable.privacy_dialog_background_large_top_small_bottom)
|
||||||
|
val middleBackground =
|
||||||
|
getMutableDrawable(R.drawable.privacy_dialog_background_small_top_small_bottom)
|
||||||
|
val lastBackground =
|
||||||
|
getMutableDrawable(R.drawable.privacy_dialog_background_small_top_large_bottom)
|
||||||
|
firstBackground.setTint(backgroundColor)
|
||||||
|
middleBackground.setTint(backgroundColor)
|
||||||
|
lastBackground.setTint(backgroundColor)
|
||||||
|
buttons.forEach { it.background = middleBackground }
|
||||||
|
buttons.first().background = firstBackground
|
||||||
|
buttons.last().background = lastBackground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureCloseAppButton(element: PrivacyElement, expandedLayout: ViewGroup): View? {
|
||||||
|
if (element.isService || !element.isActive) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val closeAppButton =
|
||||||
|
window.layoutInflater.inflate(
|
||||||
|
R.layout.privacy_dialog_card_button,
|
||||||
|
expandedLayout,
|
||||||
|
false
|
||||||
|
) as Button
|
||||||
|
expandedLayout.addView(closeAppButton)
|
||||||
|
closeAppButton.id = R.id.privacy_dialog_close_app_button
|
||||||
|
closeAppButton.setText(R.string.privacy_dialog_close_app_button)
|
||||||
|
closeAppButton.setTextColor(getForegroundColor(true))
|
||||||
|
closeAppButton.tag = element
|
||||||
|
closeAppButton.setOnClickListener { v ->
|
||||||
|
v.tag?.let {
|
||||||
|
val element = it as PrivacyElement
|
||||||
|
closeApp(element.packageName, element.userId)
|
||||||
|
closeAppTransition(element.packageName, element.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closeAppButton
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeAppTransition(packageName: String, userId: Int) {
|
||||||
|
val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
|
||||||
|
var shouldTransition = false
|
||||||
|
for (i in 0 until itemsContainer.getChildCount()) {
|
||||||
|
val itemCard = itemsContainer.getChildAt(i)
|
||||||
|
val button = itemCard.findViewById<Button>(R.id.privacy_dialog_close_app_button)
|
||||||
|
if (button == null || button.tag == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val element = button.tag as PrivacyElement
|
||||||
|
if (element.packageName != packageName || element.userId != userId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
itemCard.setEnabled(false)
|
||||||
|
|
||||||
|
val expandToggle =
|
||||||
|
itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
|
||||||
|
expandToggle.visibility = View.GONE
|
||||||
|
|
||||||
|
disableIndicatorCardUi(itemCard, element.applicationName)
|
||||||
|
|
||||||
|
val expandedLayout =
|
||||||
|
itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
|
||||||
|
if (expandedLayout.visibility == View.VISIBLE) {
|
||||||
|
expandedLayout.visibility = View.GONE
|
||||||
|
shouldTransition = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldTransition) {
|
||||||
|
ViewHierarchyAnimator.animateNextUpdate(window!!.decorView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View {
|
||||||
|
val manageButton =
|
||||||
|
window.layoutInflater.inflate(
|
||||||
|
R.layout.privacy_dialog_card_button,
|
||||||
|
expandedLayout,
|
||||||
|
false
|
||||||
|
) as Button
|
||||||
|
expandedLayout.addView(manageButton)
|
||||||
|
manageButton.id = R.id.privacy_dialog_manage_app_button
|
||||||
|
manageButton.setText(
|
||||||
|
if (element.isService) R.string.privacy_dialog_manage_service
|
||||||
|
else R.string.privacy_dialog_manage_permissions
|
||||||
|
)
|
||||||
|
manageButton.setTextColor(getForegroundColor(element.isActive))
|
||||||
|
manageButton.tag = element
|
||||||
|
manageButton.setOnClickListener { v ->
|
||||||
|
v.tag?.let {
|
||||||
|
val element = it as PrivacyElement
|
||||||
|
manageApp(element.packageName, element.userId, element.navigationIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return manageButton
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableIndicatorCardUi(itemCard: View, applicationName: CharSequence) {
|
||||||
|
val iconView = itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
|
||||||
|
val indicatorIcon = getMutableDrawable(R.drawable.privacy_dialog_check_icon)
|
||||||
|
updateIconView(iconView, indicatorIcon, false)
|
||||||
|
|
||||||
|
val closedAppText =
|
||||||
|
context.getString(R.string.privacy_dialog_close_app_message, applicationName)
|
||||||
|
val summaryView = itemCard.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
|
||||||
|
summaryView.text = closedAppText
|
||||||
|
summaryView.contentDescription = closedAppText
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setItemExpansionBehavior(itemCard: ViewGroup) {
|
||||||
|
val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
|
||||||
|
|
||||||
|
val expandToggle =
|
||||||
|
itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
|
||||||
|
expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
|
||||||
|
expandToggle.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
ViewCompat.replaceAccessibilityAction(
|
||||||
|
itemCard,
|
||||||
|
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
|
||||||
|
context.getString(R.string.privacy_dialog_expand_action),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
val expandedLayout =
|
||||||
|
itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
|
||||||
|
expandedLayout.setOnClickListener {
|
||||||
|
// Stop clicks from propagating
|
||||||
|
}
|
||||||
|
|
||||||
|
itemCard.setOnClickListener {
|
||||||
|
if (expandedLayout.visibility == View.VISIBLE) {
|
||||||
|
expandedLayout.visibility = View.GONE
|
||||||
|
expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
|
||||||
|
ViewCompat.replaceAccessibilityAction(
|
||||||
|
it!!,
|
||||||
|
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
|
||||||
|
context.getString(R.string.privacy_dialog_expand_action),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
expandedLayout.visibility = View.VISIBLE
|
||||||
|
expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up)
|
||||||
|
ViewCompat.replaceAccessibilityAction(
|
||||||
|
it!!,
|
||||||
|
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
|
||||||
|
context.getString(R.string.privacy_dialog_collapse_action),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ViewHierarchyAnimator.animateNextUpdate(
|
||||||
|
rootView = window!!.decorView,
|
||||||
|
excludedViews = setOf(expandedLayout)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateIconView(iconView: ImageView, indicatorIcon: Drawable, active: Boolean) {
|
||||||
|
indicatorIcon.setTint(getForegroundColor(active))
|
||||||
|
val backgroundIcon = getMutableDrawable(R.drawable.privacy_dialog_background_circle)
|
||||||
|
backgroundIcon.setTint(getBackgroundColor(active))
|
||||||
|
val backgroundSize =
|
||||||
|
context.resources.getDimension(R.dimen.ongoing_appops_dialog_circle_size).toInt()
|
||||||
|
val indicatorSize =
|
||||||
|
context.resources.getDimension(R.dimen.ongoing_appops_dialog_icon_size).toInt()
|
||||||
|
iconView.setImageDrawable(
|
||||||
|
constructLayeredIcon(indicatorIcon, indicatorSize, backgroundIcon, backgroundSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private fun getForegroundColor(active: Boolean) =
|
||||||
|
Utils.getColorAttrDefaultColor(
|
||||||
|
context,
|
||||||
|
if (active) com.android.internal.R.attr.materialColorOnPrimaryFixed
|
||||||
|
else com.android.internal.R.attr.materialColorOnSurface
|
||||||
|
)
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private fun getBackgroundColor(active: Boolean) =
|
||||||
|
Utils.getColorAttrDefaultColor(
|
||||||
|
context,
|
||||||
|
if (active) com.android.internal.R.attr.materialColorPrimaryFixed
|
||||||
|
else com.android.internal.R.attr.materialColorSurfaceContainerHigh
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getMutableDrawable(@DrawableRes resId: Int) = context.getDrawable(resId)!!.mutate()
|
||||||
|
|
||||||
|
private fun getUsageText(element: PrivacyElement) =
|
||||||
|
if (element.isPhoneCall) {
|
||||||
|
val phoneCallResId =
|
||||||
|
if (element.isActive) R.string.privacy_dialog_active_call_usage
|
||||||
|
else R.string.privacy_dialog_recent_call_usage
|
||||||
|
context.getString(phoneCallResId)
|
||||||
|
} else if (element.attributionLabel == null && element.proxyLabel == null) {
|
||||||
|
val usageResId: Int =
|
||||||
|
if (element.isActive) R.string.privacy_dialog_active_app_usage
|
||||||
|
else R.string.privacy_dialog_recent_app_usage
|
||||||
|
context.getString(usageResId, element.applicationName)
|
||||||
|
} else if (element.attributionLabel == null || element.proxyLabel == null) {
|
||||||
|
val singleUsageResId: Int =
|
||||||
|
if (element.isActive) R.string.privacy_dialog_active_app_usage_1
|
||||||
|
else R.string.privacy_dialog_recent_app_usage_1
|
||||||
|
context.getString(
|
||||||
|
singleUsageResId,
|
||||||
|
element.applicationName,
|
||||||
|
element.attributionLabel ?: element.proxyLabel
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val doubleUsageResId: Int =
|
||||||
|
if (element.isActive) R.string.privacy_dialog_active_app_usage_2
|
||||||
|
else R.string.privacy_dialog_recent_app_usage_2
|
||||||
|
context.getString(
|
||||||
|
doubleUsageResId,
|
||||||
|
element.applicationName,
|
||||||
|
element.attributionLabel,
|
||||||
|
element.proxyLabel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "PrivacyDialogV2"
|
||||||
|
private const val REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a permission group's icon from the system.
|
||||||
|
*
|
||||||
|
* @param groupName The name of the permission group whose icon we want
|
||||||
|
* @return The permission group's icon, the privacy_dialog_default_permission_icon icon if
|
||||||
|
* the group has no icon, or the group does not exist
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
private fun Context.getPermGroupIcon(groupName: String): Drawable {
|
||||||
|
val groupInfo = packageManager.getGroupInfo(groupName)
|
||||||
|
if (groupInfo != null && groupInfo.icon != 0) {
|
||||||
|
val icon = packageManager.loadDrawable(groupInfo.packageName, groupInfo.icon)
|
||||||
|
if (icon != null) {
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDrawable(R.drawable.privacy_dialog_default_permission_icon)!!.mutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a permission group's label from the system.
|
||||||
|
*
|
||||||
|
* @param groupName The name of the permission group whose label we want
|
||||||
|
* @return The permission group's label, or the group name, if the group is invalid
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
private fun PackageManager.getDefaultPermGroupLabel(groupName: String): CharSequence {
|
||||||
|
val groupInfo = getGroupInfo(groupName) ?: return groupName
|
||||||
|
return groupInfo.loadSafeLabel(
|
||||||
|
this,
|
||||||
|
0f,
|
||||||
|
TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [infos][PackageItemInfo] for the given permission group.
|
||||||
|
*
|
||||||
|
* @param groupName the group
|
||||||
|
* @return The info of permission group or null if the group does not have runtime
|
||||||
|
* permissions.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
private fun PackageManager.getGroupInfo(groupName: String): PackageItemInfo? {
|
||||||
|
try {
|
||||||
|
return getPermissionGroupInfo(groupName, 0)
|
||||||
|
} catch (e: NameNotFoundException) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return getPermissionInfo(groupName, 0)
|
||||||
|
} catch (e: NameNotFoundException) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun PackageManager.loadDrawable(pkg: String, @DrawableRes resId: Int): Drawable? {
|
||||||
|
return try {
|
||||||
|
getResourcesForApplication(pkg).getDrawable(resId, null)?.mutate()
|
||||||
|
} catch (e: NotFoundException) {
|
||||||
|
Log.w(LOG_TAG, "Couldn't get resource", e)
|
||||||
|
null
|
||||||
|
} catch (e: NameNotFoundException) {
|
||||||
|
Log.w(LOG_TAG, "Couldn't get resource", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun constructLayeredIcon(
|
||||||
|
icon: Drawable,
|
||||||
|
iconSize: Int,
|
||||||
|
background: Drawable,
|
||||||
|
backgroundSize: Int
|
||||||
|
): Drawable {
|
||||||
|
val layered = LayerDrawable(arrayOf(background, icon))
|
||||||
|
layered.setLayerSize(0, backgroundSize, backgroundSize)
|
||||||
|
layered.setLayerGravity(0, Gravity.CENTER)
|
||||||
|
layered.setLayerSize(1, iconSize, iconSize)
|
||||||
|
layered.setLayerGravity(1, Gravity.CENTER)
|
||||||
|
return layered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** */
|
||||||
|
data class PrivacyElement(
|
||||||
|
val type: PrivacyType,
|
||||||
|
val packageName: String,
|
||||||
|
val userId: Int,
|
||||||
|
val applicationName: CharSequence,
|
||||||
|
val attributionTag: CharSequence?,
|
||||||
|
val attributionLabel: CharSequence?,
|
||||||
|
val proxyLabel: CharSequence?,
|
||||||
|
val lastActiveTimestamp: Long,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val isPhoneCall: Boolean,
|
||||||
|
val isService: Boolean,
|
||||||
|
val permGroupName: String,
|
||||||
|
val navigationIntent: Intent
|
||||||
|
) {
|
||||||
|
private val builder = StringBuilder("PrivacyElement(")
|
||||||
|
|
||||||
|
init {
|
||||||
|
builder.append("type=${type.logName}")
|
||||||
|
builder.append(", packageName=$packageName")
|
||||||
|
builder.append(", userId=$userId")
|
||||||
|
builder.append(", appName=$applicationName")
|
||||||
|
if (attributionTag != null) {
|
||||||
|
builder.append(", attributionTag=$attributionTag")
|
||||||
|
}
|
||||||
|
if (attributionLabel != null) {
|
||||||
|
builder.append(", attributionLabel=$attributionLabel")
|
||||||
|
}
|
||||||
|
if (proxyLabel != null) {
|
||||||
|
builder.append(", proxyLabel=$proxyLabel")
|
||||||
|
}
|
||||||
|
builder.append(", lastActive=$lastActiveTimestamp")
|
||||||
|
if (isActive) {
|
||||||
|
builder.append(", active")
|
||||||
|
}
|
||||||
|
if (isPhoneCall) {
|
||||||
|
builder.append(", phoneCall")
|
||||||
|
}
|
||||||
|
if (isService) {
|
||||||
|
builder.append(", service")
|
||||||
|
}
|
||||||
|
builder.append(", permGroupName=$permGroupName")
|
||||||
|
builder.append(", navigationIntent=$navigationIntent)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = builder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** */
|
||||||
|
interface OnDialogDismissed {
|
||||||
|
fun onDialogDismissed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,11 +18,12 @@ package com.android.systemui.privacy.logging
|
|||||||
|
|
||||||
import android.icu.text.SimpleDateFormat
|
import android.icu.text.SimpleDateFormat
|
||||||
import android.permission.PermissionGroupUsage
|
import android.permission.PermissionGroupUsage
|
||||||
import com.android.systemui.log.dagger.PrivacyLog
|
|
||||||
import com.android.systemui.log.LogBuffer
|
import com.android.systemui.log.LogBuffer
|
||||||
import com.android.systemui.log.core.LogLevel
|
import com.android.systemui.log.core.LogLevel
|
||||||
import com.android.systemui.log.core.LogMessage
|
import com.android.systemui.log.core.LogMessage
|
||||||
|
import com.android.systemui.log.dagger.PrivacyLog
|
||||||
import com.android.systemui.privacy.PrivacyDialog
|
import com.android.systemui.privacy.PrivacyDialog
|
||||||
|
import com.android.systemui.privacy.PrivacyDialogV2
|
||||||
import com.android.systemui.privacy.PrivacyItem
|
import com.android.systemui.privacy.PrivacyItem
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -126,6 +127,14 @@ class PrivacyLogger @Inject constructor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logShowDialogV2Contents(contents: List<PrivacyDialogV2.PrivacyElement>) {
|
||||||
|
log(LogLevel.INFO, {
|
||||||
|
str1 = contents.toString()
|
||||||
|
}, {
|
||||||
|
"Privacy dialog shown. Contents: $str1"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fun logEmptyDialog() {
|
fun logEmptyDialog() {
|
||||||
log(LogLevel.WARNING, {}, {
|
log(LogLevel.WARNING, {}, {
|
||||||
"Trying to show an empty dialog"
|
"Trying to show an empty dialog"
|
||||||
@@ -147,6 +156,23 @@ class PrivacyLogger @Inject constructor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logCloseAppFromDialog(packageName: String, userId: Int) {
|
||||||
|
log(LogLevel.INFO, {
|
||||||
|
str1 = packageName
|
||||||
|
int1 = userId
|
||||||
|
}, {
|
||||||
|
"Close app from dialog for packageName=$str1, userId=$int1"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logStartPrivacyDashboardFromDialog() {
|
||||||
|
log(LogLevel.INFO, {}, { "Start privacy dashboard from dialog" })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logLabelNotFound(packageName: String) {
|
||||||
|
log(LogLevel.WARNING, { str1 = packageName }, { "Label not found for: $str1" })
|
||||||
|
}
|
||||||
|
|
||||||
private fun listToString(list: List<PrivacyItem>): String {
|
private fun listToString(list: List<PrivacyItem>): String {
|
||||||
return list.joinToString(separator = ", ", transform = PrivacyItem::log)
|
return list.joinToString(separator = ", ", transform = PrivacyItem::log)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ import com.android.internal.logging.UiEventLogger
|
|||||||
import com.android.systemui.animation.ActivityLaunchAnimator
|
import com.android.systemui.animation.ActivityLaunchAnimator
|
||||||
import com.android.systemui.appops.AppOpsController
|
import com.android.systemui.appops.AppOpsController
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||||
|
import com.android.systemui.flags.FeatureFlags
|
||||||
|
import com.android.systemui.flags.Flags
|
||||||
import com.android.systemui.plugins.ActivityStarter
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
import com.android.systemui.privacy.OngoingPrivacyChip
|
import com.android.systemui.privacy.OngoingPrivacyChip
|
||||||
import com.android.systemui.privacy.PrivacyChipEvent
|
import com.android.systemui.privacy.PrivacyChipEvent
|
||||||
import com.android.systemui.privacy.PrivacyDialogController
|
import com.android.systemui.privacy.PrivacyDialogController
|
||||||
|
import com.android.systemui.privacy.PrivacyDialogControllerV2
|
||||||
import com.android.systemui.privacy.PrivacyItem
|
import com.android.systemui.privacy.PrivacyItem
|
||||||
import com.android.systemui.privacy.PrivacyItemController
|
import com.android.systemui.privacy.PrivacyItemController
|
||||||
import com.android.systemui.privacy.logging.PrivacyLogger
|
import com.android.systemui.privacy.logging.PrivacyLogger
|
||||||
@@ -49,6 +52,7 @@ class HeaderPrivacyIconsController @Inject constructor(
|
|||||||
private val uiEventLogger: UiEventLogger,
|
private val uiEventLogger: UiEventLogger,
|
||||||
@Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip,
|
@Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip,
|
||||||
private val privacyDialogController: PrivacyDialogController,
|
private val privacyDialogController: PrivacyDialogController,
|
||||||
|
private val privacyDialogControllerV2: PrivacyDialogControllerV2,
|
||||||
private val privacyLogger: PrivacyLogger,
|
private val privacyLogger: PrivacyLogger,
|
||||||
@Named(SHADE_HEADER) private val iconContainer: StatusIconContainer,
|
@Named(SHADE_HEADER) private val iconContainer: StatusIconContainer,
|
||||||
private val permissionManager: PermissionManager,
|
private val permissionManager: PermissionManager,
|
||||||
@@ -58,7 +62,8 @@ class HeaderPrivacyIconsController @Inject constructor(
|
|||||||
private val appOpsController: AppOpsController,
|
private val appOpsController: AppOpsController,
|
||||||
private val broadcastDispatcher: BroadcastDispatcher,
|
private val broadcastDispatcher: BroadcastDispatcher,
|
||||||
private val safetyCenterManager: SafetyCenterManager,
|
private val safetyCenterManager: SafetyCenterManager,
|
||||||
private val deviceProvisionedController: DeviceProvisionedController
|
private val deviceProvisionedController: DeviceProvisionedController,
|
||||||
|
private val featureFlags: FeatureFlags
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var chipVisibilityListener: ChipVisibilityListener? = null
|
var chipVisibilityListener: ChipVisibilityListener? = null
|
||||||
@@ -143,7 +148,11 @@ class HeaderPrivacyIconsController @Inject constructor(
|
|||||||
// If the privacy chip is visible, it means there were some indicators
|
// If the privacy chip is visible, it means there were some indicators
|
||||||
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
|
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
|
||||||
if (safetyCenterEnabled) {
|
if (safetyCenterEnabled) {
|
||||||
showSafetyCenter()
|
if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) {
|
||||||
|
privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip)
|
||||||
|
} else {
|
||||||
|
showSafetyCenter()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
privacyDialogController.showDialog(privacyChip.context)
|
privacyDialogController.showDialog(privacyChip.context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,825 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.privacy
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.PackageManager.ResolveInfoFlags
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.content.pm.UserInfo
|
||||||
|
import android.os.Process.SYSTEM_UID
|
||||||
|
import android.os.UserHandle
|
||||||
|
import android.permission.PermissionGroupUsage
|
||||||
|
import android.permission.PermissionManager
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import android.view.View
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.internal.logging.UiEventLogger
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.animation.DialogLaunchAnimator
|
||||||
|
import com.android.systemui.appops.AppOpsController
|
||||||
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
|
import com.android.systemui.privacy.logging.PrivacyLogger
|
||||||
|
import com.android.systemui.settings.UserTracker
|
||||||
|
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||||
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
|
import com.android.systemui.util.mockito.capture
|
||||||
|
import com.android.systemui.util.mockito.any
|
||||||
|
import com.android.systemui.util.mockito.eq
|
||||||
|
import com.android.systemui.util.time.FakeSystemClock
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
import org.mockito.ArgumentMatchers.anyBoolean
|
||||||
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
|
import org.mockito.Captor
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
|
import org.mockito.Mockito.atLeastOnce
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.times
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
class PrivacyDialogControllerV2Test : SysuiTestCase() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val USER_ID = 0
|
||||||
|
private const val ENT_USER_ID = 10
|
||||||
|
|
||||||
|
private const val TEST_PACKAGE_NAME = "test package name"
|
||||||
|
private const val TEST_ATTRIBUTION_TAG = "test attribution tag"
|
||||||
|
private const val TEST_PROXY_LABEL = "test proxy label"
|
||||||
|
|
||||||
|
private const val PERM_CAMERA = android.Manifest.permission_group.CAMERA
|
||||||
|
private const val PERM_MICROPHONE = android.Manifest.permission_group.MICROPHONE
|
||||||
|
private const val PERM_LOCATION = android.Manifest.permission_group.LOCATION
|
||||||
|
|
||||||
|
private val TEST_INTENT = Intent("test_intent_action")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var dialog: PrivacyDialogV2
|
||||||
|
@Mock
|
||||||
|
private lateinit var permissionManager: PermissionManager
|
||||||
|
@Mock
|
||||||
|
private lateinit var packageManager: PackageManager
|
||||||
|
@Mock
|
||||||
|
private lateinit var privacyItemController: PrivacyItemController
|
||||||
|
@Mock
|
||||||
|
private lateinit var userTracker: UserTracker
|
||||||
|
@Mock
|
||||||
|
private lateinit var activityStarter: ActivityStarter
|
||||||
|
@Mock
|
||||||
|
private lateinit var privacyLogger: PrivacyLogger
|
||||||
|
@Mock
|
||||||
|
private lateinit var keyguardStateController: KeyguardStateController
|
||||||
|
@Mock
|
||||||
|
private lateinit var appOpsController: AppOpsController
|
||||||
|
@Captor
|
||||||
|
private lateinit var dialogDismissedCaptor: ArgumentCaptor<PrivacyDialogV2.OnDialogDismissed>
|
||||||
|
@Captor
|
||||||
|
private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
|
||||||
|
@Captor
|
||||||
|
private lateinit var intentCaptor: ArgumentCaptor<Intent>
|
||||||
|
@Mock
|
||||||
|
private lateinit var uiEventLogger: UiEventLogger
|
||||||
|
@Mock
|
||||||
|
private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
|
||||||
|
|
||||||
|
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
|
||||||
|
private val uiExecutor = FakeExecutor(FakeSystemClock())
|
||||||
|
private lateinit var controller: PrivacyDialogControllerV2
|
||||||
|
private var nextUid: Int = 0
|
||||||
|
|
||||||
|
private val dialogProvider = object : PrivacyDialogControllerV2.DialogProvider {
|
||||||
|
var list: List<PrivacyDialogV2.PrivacyElement>? = null
|
||||||
|
var manageApp: ((String, Int, Intent) -> Unit)? = null
|
||||||
|
var closeApp: ((String, Int) -> Unit)? = null
|
||||||
|
var openPrivacyDashboard: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun makeDialog(
|
||||||
|
context: Context,
|
||||||
|
list: List<PrivacyDialogV2.PrivacyElement>,
|
||||||
|
manageApp: (String, Int, Intent) -> Unit,
|
||||||
|
closeApp: (String, Int) -> Unit,
|
||||||
|
openPrivacyDashboard: () -> Unit
|
||||||
|
): PrivacyDialogV2 {
|
||||||
|
this.list = list
|
||||||
|
this.manageApp = manageApp
|
||||||
|
this.closeApp = closeApp
|
||||||
|
this.openPrivacyDashboard = openPrivacyDashboard
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
nextUid = 0
|
||||||
|
setUpDefaultMockResponses()
|
||||||
|
|
||||||
|
controller = PrivacyDialogControllerV2(
|
||||||
|
permissionManager,
|
||||||
|
packageManager,
|
||||||
|
privacyItemController,
|
||||||
|
userTracker,
|
||||||
|
activityStarter,
|
||||||
|
backgroundExecutor,
|
||||||
|
uiExecutor,
|
||||||
|
privacyLogger,
|
||||||
|
keyguardStateController,
|
||||||
|
appOpsController,
|
||||||
|
uiEventLogger,
|
||||||
|
dialogLaunchAnimator,
|
||||||
|
dialogProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
|
||||||
|
dialogProvider.list = null
|
||||||
|
dialogProvider.manageApp = null
|
||||||
|
dialogProvider.closeApp = null
|
||||||
|
dialogProvider.openPrivacyDashboard = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMicMutedParameter() {
|
||||||
|
`when`(appOpsController.isMicMuted).thenReturn(true)
|
||||||
|
controller.showDialog(context)
|
||||||
|
backgroundExecutor.runAllReady()
|
||||||
|
|
||||||
|
verify(permissionManager).getIndicatorAppOpUsageData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPermissionManagerOnlyCalledInBackgroundThread() {
|
||||||
|
controller.showDialog(context)
|
||||||
|
verify(permissionManager, never()).getIndicatorAppOpUsageData(anyBoolean())
|
||||||
|
backgroundExecutor.runAllReady()
|
||||||
|
verify(permissionManager).getIndicatorAppOpUsageData(anyBoolean())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPackageManagerOnlyCalledInBackgroundThread() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(usage.isPhoneCall).thenReturn(false)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
verify(packageManager, never()).getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
|
||||||
|
backgroundExecutor.runAllReady()
|
||||||
|
verify(packageManager, atLeastOnce())
|
||||||
|
.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShowDialogShowsDialogWithoutView() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), anyBoolean())
|
||||||
|
verify(dialog).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShowDialogShowsDialogWithView() {
|
||||||
|
val view = View(context)
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
|
||||||
|
controller.showDialog(context, view)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
verify(dialogLaunchAnimator).showFromView(dialog, view)
|
||||||
|
verify(dialog, never()).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDontShowEmptyDialog() {
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
verify(dialog, never()).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHideDialogDismissesDialogIfShown() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
controller.dismissDialog()
|
||||||
|
verify(dialog).dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHideDialogNoopIfNotShown() {
|
||||||
|
controller.dismissDialog()
|
||||||
|
verify(dialog, never()).dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHideDialogNoopAfterDismissed() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
|
||||||
|
|
||||||
|
dialogDismissedCaptor.value.onDialogDismissed()
|
||||||
|
controller.dismissDialog()
|
||||||
|
verify(dialog, never()).dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testShowForAllUsers() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
|
||||||
|
exhaustExecutors()
|
||||||
|
verify(dialog).setShowForAllUsers(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSingleElementInList() {
|
||||||
|
val usage = createMockPermGroupUsage(
|
||||||
|
packageName = TEST_PACKAGE_NAME,
|
||||||
|
uid = generateUidForUser(USER_ID),
|
||||||
|
permissionGroupName = PERM_CAMERA,
|
||||||
|
lastAccessTimeMillis = 5L,
|
||||||
|
isActive = true,
|
||||||
|
isPhoneCall = false,
|
||||||
|
attributionTag = null,
|
||||||
|
proxyLabel = TEST_PROXY_LABEL
|
||||||
|
)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.list?.let { list ->
|
||||||
|
assertThat(list.get(0).type).isEqualTo(PrivacyType.TYPE_CAMERA)
|
||||||
|
assertThat(list.get(0).packageName).isEqualTo(TEST_PACKAGE_NAME)
|
||||||
|
assertThat(list.get(0).userId).isEqualTo(USER_ID)
|
||||||
|
assertThat(list.get(0).applicationName).isEqualTo(TEST_PACKAGE_NAME)
|
||||||
|
assertThat(list.get(0).attributionTag).isNull()
|
||||||
|
assertThat(list.get(0).attributionLabel).isNull()
|
||||||
|
assertThat(list.get(0).proxyLabel).isEqualTo(TEST_PROXY_LABEL)
|
||||||
|
assertThat(list.get(0).lastActiveTimestamp).isEqualTo(5L)
|
||||||
|
assertThat(list.get(0).isActive).isTrue()
|
||||||
|
assertThat(list.get(0).isPhoneCall).isFalse()
|
||||||
|
assertThat(list.get(0).isService).isFalse()
|
||||||
|
assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA)
|
||||||
|
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
|
||||||
|
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
|
||||||
|
.isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isIntentEqual(actual: Intent, expected: Intent): Boolean {
|
||||||
|
return actual.action == expected.action &&
|
||||||
|
actual.getStringExtra(Intent.EXTRA_PACKAGE_NAME) ==
|
||||||
|
expected.getStringExtra(Intent.EXTRA_PACKAGE_NAME) &&
|
||||||
|
actual.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle ==
|
||||||
|
expected.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTwoElementsDifferentType_sorted() {
|
||||||
|
val usage_camera = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_camera",
|
||||||
|
permissionGroupName = PERM_CAMERA
|
||||||
|
)
|
||||||
|
val usage_microphone = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_microphone",
|
||||||
|
permissionGroupName = PERM_MICROPHONE
|
||||||
|
)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
|
||||||
|
listOf(usage_microphone, usage_camera)
|
||||||
|
)
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.list?.let { list ->
|
||||||
|
assertThat(list).hasSize(2)
|
||||||
|
assertThat(list.get(0).type.compareTo(list.get(1).type)).isLessThan(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTwoElementsSameType_oneActive() {
|
||||||
|
val usage_active = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_active",
|
||||||
|
isActive = true
|
||||||
|
)
|
||||||
|
val usage_recent = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_recent",
|
||||||
|
isActive = false
|
||||||
|
)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
|
||||||
|
listOf(usage_recent, usage_active)
|
||||||
|
)
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
assertThat(dialogProvider.list).hasSize(1)
|
||||||
|
assertThat(dialogProvider.list?.get(0)?.isActive).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTwoElementsSameType_twoActive() {
|
||||||
|
val usage_active = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_active",
|
||||||
|
isActive = true,
|
||||||
|
lastAccessTimeMillis = 0L
|
||||||
|
)
|
||||||
|
val usage_active_moreRecent = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_active_recent",
|
||||||
|
isActive = true,
|
||||||
|
lastAccessTimeMillis = 1L
|
||||||
|
)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
|
||||||
|
listOf(usage_active, usage_active_moreRecent)
|
||||||
|
)
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
assertThat(dialogProvider.list).hasSize(2)
|
||||||
|
assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L)
|
||||||
|
assertThat(dialogProvider.list?.get(1)?.lastActiveTimestamp).isEqualTo(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testManyElementsSameType_bothRecent() {
|
||||||
|
val usage_recent = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_recent",
|
||||||
|
isActive = false,
|
||||||
|
lastAccessTimeMillis = 0L
|
||||||
|
)
|
||||||
|
val usage_moreRecent = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_moreRecent",
|
||||||
|
isActive = false,
|
||||||
|
lastAccessTimeMillis = 1L
|
||||||
|
)
|
||||||
|
val usage_mostRecent = createMockPermGroupUsage(
|
||||||
|
packageName = "${TEST_PACKAGE_NAME}_mostRecent",
|
||||||
|
isActive = false,
|
||||||
|
lastAccessTimeMillis = 2L
|
||||||
|
)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
|
||||||
|
listOf(usage_recent, usage_mostRecent, usage_moreRecent)
|
||||||
|
)
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
assertThat(dialogProvider.list).hasSize(1)
|
||||||
|
assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(2L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMicAndCameraDisabled() {
|
||||||
|
val usage_camera = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_CAMERA
|
||||||
|
)
|
||||||
|
val usage_microphone = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_MICROPHONE
|
||||||
|
)
|
||||||
|
val usage_location = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_LOCATION
|
||||||
|
)
|
||||||
|
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
|
||||||
|
listOf(usage_camera, usage_location, usage_microphone)
|
||||||
|
)
|
||||||
|
`when`(privacyItemController.micCameraAvailable).thenReturn(false)
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
assertThat(dialogProvider.list).hasSize(1)
|
||||||
|
assertThat(dialogProvider.list?.get(0)?.type).isEqualTo(PrivacyType.TYPE_LOCATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLocationDisabled() {
|
||||||
|
val usage_camera = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_CAMERA
|
||||||
|
)
|
||||||
|
val usage_microphone = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_MICROPHONE
|
||||||
|
)
|
||||||
|
val usage_location = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_LOCATION
|
||||||
|
)
|
||||||
|
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
|
||||||
|
listOf(usage_camera, usage_location, usage_microphone)
|
||||||
|
)
|
||||||
|
`when`(privacyItemController.locationAvailable).thenReturn(false)
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
assertThat(dialogProvider.list).hasSize(2)
|
||||||
|
dialogProvider.list?.forEach {
|
||||||
|
assertThat(it.type).isNotEqualTo(PrivacyType.TYPE_LOCATION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAllIndicatorsAvailable() {
|
||||||
|
val usage_camera = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_CAMERA
|
||||||
|
)
|
||||||
|
val usage_microphone = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_MICROPHONE
|
||||||
|
)
|
||||||
|
val usage_location = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_LOCATION
|
||||||
|
)
|
||||||
|
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
|
||||||
|
listOf(usage_camera, usage_location, usage_microphone)
|
||||||
|
)
|
||||||
|
`when`(privacyItemController.micCameraAvailable).thenReturn(true)
|
||||||
|
`when`(privacyItemController.locationAvailable).thenReturn(true)
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
assertThat(dialogProvider.list).hasSize(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNoIndicatorsAvailable() {
|
||||||
|
val usage_camera = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_CAMERA
|
||||||
|
)
|
||||||
|
val usage_microphone = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_MICROPHONE
|
||||||
|
)
|
||||||
|
val usage_location = createMockPermGroupUsage(
|
||||||
|
permissionGroupName = PERM_LOCATION
|
||||||
|
)
|
||||||
|
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
|
||||||
|
listOf(usage_camera, usage_location, usage_microphone)
|
||||||
|
)
|
||||||
|
`when`(privacyItemController.micCameraAvailable).thenReturn(false)
|
||||||
|
`when`(privacyItemController.locationAvailable).thenReturn(false)
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
verify(dialog, never()).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNotCurrentUser() {
|
||||||
|
val usage_other = createMockPermGroupUsage(
|
||||||
|
uid = generateUidForUser(ENT_USER_ID + 1)
|
||||||
|
)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean()))
|
||||||
|
.thenReturn(listOf(usage_other))
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
verify(dialog, never()).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStartActivitySuccess() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
|
||||||
|
verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
|
||||||
|
|
||||||
|
activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP)
|
||||||
|
|
||||||
|
verify(dialog).dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStartActivityFailure() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
|
||||||
|
verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
|
||||||
|
|
||||||
|
activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
|
||||||
|
|
||||||
|
verify(dialog, never()).dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCallOnSecondaryUser() {
|
||||||
|
// Calls happen in
|
||||||
|
val usage = createMockPermGroupUsage(uid = SYSTEM_UID, isPhoneCall = true)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
`when`(userTracker.userProfiles).thenReturn(listOf(
|
||||||
|
UserInfo(ENT_USER_ID, "", 0)
|
||||||
|
))
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
verify(dialog).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testManageAppLogs() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
|
||||||
|
verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
|
||||||
|
USER_ID, TEST_PACKAGE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCloseAppLogs() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.closeApp?.invoke(TEST_PACKAGE_NAME, USER_ID)
|
||||||
|
verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
|
||||||
|
USER_ID, TEST_PACKAGE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOpenPrivacyDashboardLogs() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.openPrivacyDashboard?.invoke()
|
||||||
|
verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDismissedDialogLogs() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
|
||||||
|
|
||||||
|
dialogDismissedCaptor.value.onDialogDismissed()
|
||||||
|
|
||||||
|
controller.dismissDialog()
|
||||||
|
|
||||||
|
verify(uiEventLogger, times(1)).log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDefaultIntent() {
|
||||||
|
val usage = createMockPermGroupUsage()
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.list?.let { list ->
|
||||||
|
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
|
||||||
|
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
|
||||||
|
.isTrue()
|
||||||
|
assertThat(list.get(0).isService).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDefaultIntentOnEnterpriseUser() {
|
||||||
|
val usage =
|
||||||
|
createMockPermGroupUsage(
|
||||||
|
uid = generateUidForUser(ENT_USER_ID),
|
||||||
|
)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.list?.let { list ->
|
||||||
|
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
|
||||||
|
controller.getDefaultManageAppPermissionsIntent(
|
||||||
|
TEST_PACKAGE_NAME, ENT_USER_ID)))
|
||||||
|
.isTrue()
|
||||||
|
assertThat(list.get(0).isService).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDefaultIntentOnInvalidAttributionTag() {
|
||||||
|
val usage = createMockPermGroupUsage(
|
||||||
|
attributionTag = "INVALID_ATTRIBUTION_TAG",
|
||||||
|
proxyLabel = TEST_PROXY_LABEL
|
||||||
|
)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.list?.let { list ->
|
||||||
|
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
|
||||||
|
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
|
||||||
|
.isTrue()
|
||||||
|
assertThat(list.get(0).isService).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testServiceIntentOnCorrectSubAttribution() {
|
||||||
|
val usage = createMockPermGroupUsage(
|
||||||
|
attributionTag = TEST_ATTRIBUTION_TAG,
|
||||||
|
attributionLabel = "TEST_LABEL"
|
||||||
|
)
|
||||||
|
|
||||||
|
val activityInfo = createMockActivityInfo()
|
||||||
|
val resolveInfo = createMockResolveInfo(activityInfo)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
`when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
|
||||||
|
.thenAnswer { resolveInfo }
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.list?.let { list ->
|
||||||
|
val navigationIntent = list.get(0).navigationIntent!!
|
||||||
|
assertThat(navigationIntent.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_USAGE)
|
||||||
|
assertThat(navigationIntent.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME))
|
||||||
|
.isEqualTo(PERM_CAMERA)
|
||||||
|
assertThat(navigationIntent.getStringArrayExtra(Intent.EXTRA_ATTRIBUTION_TAGS))
|
||||||
|
.isEqualTo(arrayOf(TEST_ATTRIBUTION_TAG.toString()))
|
||||||
|
assertThat(navigationIntent.getBooleanExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, false))
|
||||||
|
.isTrue()
|
||||||
|
assertThat(list.get(0).isService).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDefaultIntentOnMissingAttributionLabel() {
|
||||||
|
val usage = createMockPermGroupUsage(
|
||||||
|
attributionTag = TEST_ATTRIBUTION_TAG
|
||||||
|
)
|
||||||
|
|
||||||
|
val activityInfo = createMockActivityInfo()
|
||||||
|
val resolveInfo = createMockResolveInfo(activityInfo)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
`when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
|
||||||
|
.thenAnswer { resolveInfo }
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.list?.let { list ->
|
||||||
|
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
|
||||||
|
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
|
||||||
|
.isTrue()
|
||||||
|
assertThat(list.get(0).isService).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDefaultIntentOnIncorrectPermission() {
|
||||||
|
val usage = createMockPermGroupUsage(
|
||||||
|
attributionTag = TEST_ATTRIBUTION_TAG
|
||||||
|
)
|
||||||
|
|
||||||
|
val activityInfo = createMockActivityInfo(
|
||||||
|
permission = "INCORRECT_PERMISSION"
|
||||||
|
)
|
||||||
|
val resolveInfo = createMockResolveInfo(activityInfo)
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
|
||||||
|
`when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
|
||||||
|
.thenAnswer { resolveInfo }
|
||||||
|
controller.showDialog(context)
|
||||||
|
exhaustExecutors()
|
||||||
|
|
||||||
|
dialogProvider.list?.let { list ->
|
||||||
|
assertThat(isIntentEqual(list.get(0).navigationIntent!!,
|
||||||
|
controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
|
||||||
|
.isTrue()
|
||||||
|
assertThat(list.get(0).isService).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exhaustExecutors() {
|
||||||
|
FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpDefaultMockResponses() {
|
||||||
|
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(emptyList())
|
||||||
|
`when`(appOpsController.isMicMuted).thenReturn(false)
|
||||||
|
|
||||||
|
`when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
|
||||||
|
.thenAnswer { FakeApplicationInfo(it.getArgument(0)) }
|
||||||
|
|
||||||
|
`when`(privacyItemController.locationAvailable).thenReturn(true)
|
||||||
|
`when`(privacyItemController.micCameraAvailable).thenReturn(true)
|
||||||
|
|
||||||
|
`when`(userTracker.userProfiles).thenReturn(listOf(
|
||||||
|
UserInfo(USER_ID, "", 0),
|
||||||
|
UserInfo(ENT_USER_ID, "", UserInfo.FLAG_MANAGED_PROFILE)
|
||||||
|
))
|
||||||
|
|
||||||
|
`when`(keyguardStateController.isUnlocked).thenReturn(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeApplicationInfo(val label: CharSequence) : ApplicationInfo() {
|
||||||
|
override fun loadLabel(pm: PackageManager): CharSequence {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateUidForUser(user: Int): Int {
|
||||||
|
return user * UserHandle.PER_USER_RANGE + nextUid++
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMockResolveInfo(
|
||||||
|
activityInfo: ActivityInfo? = null
|
||||||
|
): ResolveInfo {
|
||||||
|
val resolveInfo = mock(ResolveInfo::class.java)
|
||||||
|
resolveInfo.activityInfo = activityInfo
|
||||||
|
return resolveInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMockActivityInfo(
|
||||||
|
permission: String = android.Manifest.permission.START_VIEW_PERMISSION_USAGE,
|
||||||
|
className: String = "TEST_CLASS_NAME"
|
||||||
|
): ActivityInfo {
|
||||||
|
val activityInfo = mock(ActivityInfo::class.java)
|
||||||
|
activityInfo.permission = permission
|
||||||
|
activityInfo.name = className
|
||||||
|
return activityInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMockPermGroupUsage(
|
||||||
|
packageName: String = TEST_PACKAGE_NAME,
|
||||||
|
uid: Int = generateUidForUser(USER_ID),
|
||||||
|
permissionGroupName: String = PERM_CAMERA,
|
||||||
|
lastAccessTimeMillis: Long = 0L,
|
||||||
|
isActive: Boolean = false,
|
||||||
|
isPhoneCall: Boolean = false,
|
||||||
|
attributionTag: CharSequence? = null,
|
||||||
|
attributionLabel: CharSequence? = null,
|
||||||
|
proxyLabel: CharSequence? = null
|
||||||
|
): PermissionGroupUsage {
|
||||||
|
val usage = mock(PermissionGroupUsage::class.java)
|
||||||
|
`when`(usage.packageName).thenReturn(packageName)
|
||||||
|
`when`(usage.uid).thenReturn(uid)
|
||||||
|
`when`(usage.permissionGroupName).thenReturn(permissionGroupName)
|
||||||
|
`when`(usage.lastAccessTimeMillis).thenReturn(lastAccessTimeMillis)
|
||||||
|
`when`(usage.isActive).thenReturn(isActive)
|
||||||
|
`when`(usage.isPhoneCall).thenReturn(isPhoneCall)
|
||||||
|
`when`(usage.attributionTag).thenReturn(attributionTag)
|
||||||
|
`when`(usage.attributionLabel).thenReturn(attributionLabel)
|
||||||
|
`when`(usage.proxyLabel).thenReturn(proxyLabel)
|
||||||
|
return usage
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.privacy
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import android.testing.TestableLooper
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||||
|
class PrivacyDialogV2Test : SysuiTestCase() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TEST_PACKAGE_NAME = "test_pkg"
|
||||||
|
private const val TEST_USER_ID = 0
|
||||||
|
private const val TEST_PERM_GROUP = "test_perm_group"
|
||||||
|
|
||||||
|
private val TEST_INTENT = Intent("test_intent_action")
|
||||||
|
|
||||||
|
private fun createPrivacyElement(
|
||||||
|
type: PrivacyType = PrivacyType.TYPE_MICROPHONE,
|
||||||
|
packageName: String = TEST_PACKAGE_NAME,
|
||||||
|
userId: Int = TEST_USER_ID,
|
||||||
|
applicationName: CharSequence = "App",
|
||||||
|
attributionTag: CharSequence? = null,
|
||||||
|
attributionLabel: CharSequence? = null,
|
||||||
|
proxyLabel: CharSequence? = null,
|
||||||
|
lastActiveTimestamp: Long = 0L,
|
||||||
|
isActive: Boolean = false,
|
||||||
|
isPhoneCall: Boolean = false,
|
||||||
|
isService: Boolean = false,
|
||||||
|
permGroupName: String = TEST_PERM_GROUP,
|
||||||
|
navigationIntent: Intent = TEST_INTENT
|
||||||
|
) =
|
||||||
|
PrivacyDialogV2.PrivacyElement(
|
||||||
|
type,
|
||||||
|
packageName,
|
||||||
|
userId,
|
||||||
|
applicationName,
|
||||||
|
attributionTag,
|
||||||
|
attributionLabel,
|
||||||
|
proxyLabel,
|
||||||
|
lastActiveTimestamp,
|
||||||
|
isActive,
|
||||||
|
isPhoneCall,
|
||||||
|
isService,
|
||||||
|
permGroupName,
|
||||||
|
navigationIntent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mock private lateinit var manageApp: (String, Int, Intent) -> Unit
|
||||||
|
@Mock private lateinit var closeApp: (String, Int) -> Unit
|
||||||
|
@Mock private lateinit var openPrivacyDashboard: () -> Unit
|
||||||
|
private lateinit var dialog: PrivacyDialogV2
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun teardown() {
|
||||||
|
if (this::dialog.isInitialized) {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testManageAppCalledWithCorrectParams() {
|
||||||
|
val list = listOf(createPrivacyElement())
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
dialog.requireViewById<View>(R.id.privacy_dialog_manage_app_button).callOnClick()
|
||||||
|
|
||||||
|
verify(manageApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID, TEST_INTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCloseAppCalledWithCorrectParams() {
|
||||||
|
val list = listOf(createPrivacyElement(isActive = true))
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
dialog.requireViewById<View>(R.id.privacy_dialog_close_app_button).callOnClick()
|
||||||
|
|
||||||
|
verify(closeApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCloseAppMissingForService() {
|
||||||
|
val list = listOf(createPrivacyElement(isActive = true, isService = true))
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.findViewById<View>(R.id.privacy_dialog_manage_app_button)).isNotNull()
|
||||||
|
assertThat(dialog.findViewById<View>(R.id.privacy_dialog_close_app_button)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoreButton() {
|
||||||
|
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
dialog.requireViewById<View>(R.id.privacy_dialog_more_button).callOnClick()
|
||||||
|
|
||||||
|
verify(openPrivacyDashboard).invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCloseButton() {
|
||||||
|
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
|
||||||
|
dialog.addOnDismissListener(dismissListener)
|
||||||
|
dialog.show()
|
||||||
|
verify(dismissListener, never()).onDialogDismissed()
|
||||||
|
|
||||||
|
dialog.requireViewById<View>(R.id.privacy_dialog_close_button).callOnClick()
|
||||||
|
|
||||||
|
verify(dismissListener).onDialogDismissed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDismissListenerCalledOnDismiss() {
|
||||||
|
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
|
||||||
|
dialog.addOnDismissListener(dismissListener)
|
||||||
|
dialog.show()
|
||||||
|
verify(dismissListener, never()).onDialogDismissed()
|
||||||
|
|
||||||
|
dialog.dismiss()
|
||||||
|
|
||||||
|
verify(dismissListener).onDialogDismissed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() {
|
||||||
|
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
|
||||||
|
dialog.show()
|
||||||
|
dialog.dismiss()
|
||||||
|
|
||||||
|
dialog.addOnDismissListener(dismissListener)
|
||||||
|
|
||||||
|
verify(dismissListener).onDialogDismissed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCorrectNumElements() {
|
||||||
|
val list =
|
||||||
|
listOf(
|
||||||
|
createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true),
|
||||||
|
createPrivacyElement()
|
||||||
|
)
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
dialog.requireViewById<ViewGroup>(R.id.privacy_dialog_items_container).childCount
|
||||||
|
)
|
||||||
|
.isEqualTo(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHeaderText() {
|
||||||
|
val list = listOf(createPrivacyElement())
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_title).text)
|
||||||
|
.isEqualTo(TEST_PERM_GROUP)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUsingText() {
|
||||||
|
val list = listOf(createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true))
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
|
||||||
|
.isEqualTo("In use by App")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRecentText() {
|
||||||
|
val list = listOf(createPrivacyElement())
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
|
||||||
|
.isEqualTo("Recently used by App")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPhoneCall() {
|
||||||
|
val list = listOf(createPrivacyElement(isPhoneCall = true))
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
|
||||||
|
.isEqualTo("Recently used in phone call")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPhoneCallNotClickable() {
|
||||||
|
val list = listOf(createPrivacyElement(isPhoneCall = true))
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.requireViewById<View>(R.id.privacy_dialog_item_card).isClickable)
|
||||||
|
.isFalse()
|
||||||
|
assertThat(
|
||||||
|
dialog
|
||||||
|
.requireViewById<View>(R.id.privacy_dialog_item_header_expand_toggle)
|
||||||
|
.visibility
|
||||||
|
)
|
||||||
|
.isEqualTo(View.GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testProxyLabel() {
|
||||||
|
val list = listOf(createPrivacyElement(proxyLabel = "proxy label"))
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
|
||||||
|
.isEqualTo("Recently used by App (proxy label)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSubattribution() {
|
||||||
|
val list =
|
||||||
|
listOf(
|
||||||
|
createPrivacyElement(
|
||||||
|
attributionLabel = "For subattribution",
|
||||||
|
isActive = true,
|
||||||
|
isService = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
|
||||||
|
.isEqualTo("In use by App (For subattribution)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSubattributionAndProxyLabel() {
|
||||||
|
val list =
|
||||||
|
listOf(
|
||||||
|
createPrivacyElement(
|
||||||
|
attributionLabel = "For subattribution",
|
||||||
|
proxyLabel = "proxy label",
|
||||||
|
isActive = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
dialog.show()
|
||||||
|
assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
|
||||||
|
.isEqualTo("In use by App (For subattribution \u2022 proxy label)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDialogHasTitle() {
|
||||||
|
val list = listOf(createPrivacyElement())
|
||||||
|
dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.window?.attributes?.title).isEqualTo("Microphone & Camera")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDialogIsFullscreen() {
|
||||||
|
dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
assertThat(dialog.window?.attributes?.width).isEqualTo(MATCH_PARENT)
|
||||||
|
assertThat(dialog.window?.attributes?.height).isEqualTo(MATCH_PARENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,12 @@ import com.android.internal.logging.UiEventLogger
|
|||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
import com.android.systemui.appops.AppOpsController
|
import com.android.systemui.appops.AppOpsController
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||||
|
import com.android.systemui.flags.FeatureFlags
|
||||||
|
import com.android.systemui.flags.Flags
|
||||||
import com.android.systemui.plugins.ActivityStarter
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
import com.android.systemui.privacy.OngoingPrivacyChip
|
import com.android.systemui.privacy.OngoingPrivacyChip
|
||||||
import com.android.systemui.privacy.PrivacyDialogController
|
import com.android.systemui.privacy.PrivacyDialogController
|
||||||
|
import com.android.systemui.privacy.PrivacyDialogControllerV2
|
||||||
import com.android.systemui.privacy.PrivacyItemController
|
import com.android.systemui.privacy.PrivacyItemController
|
||||||
import com.android.systemui.privacy.logging.PrivacyLogger
|
import com.android.systemui.privacy.logging.PrivacyLogger
|
||||||
import com.android.systemui.statusbar.phone.StatusIconContainer
|
import com.android.systemui.statusbar.phone.StatusIconContainer
|
||||||
@@ -24,6 +27,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
|
|||||||
import com.android.systemui.util.mockito.any
|
import com.android.systemui.util.mockito.any
|
||||||
import com.android.systemui.util.mockito.argumentCaptor
|
import com.android.systemui.util.mockito.argumentCaptor
|
||||||
import com.android.systemui.util.mockito.capture
|
import com.android.systemui.util.mockito.capture
|
||||||
|
import com.android.systemui.util.mockito.eq
|
||||||
import com.android.systemui.util.mockito.nullable
|
import com.android.systemui.util.mockito.nullable
|
||||||
import com.android.systemui.util.time.FakeSystemClock
|
import com.android.systemui.util.time.FakeSystemClock
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -54,6 +58,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var privacyDialogController: PrivacyDialogController
|
private lateinit var privacyDialogController: PrivacyDialogController
|
||||||
@Mock
|
@Mock
|
||||||
|
private lateinit var privacyDialogControllerV2: PrivacyDialogControllerV2
|
||||||
|
@Mock
|
||||||
private lateinit var privacyLogger: PrivacyLogger
|
private lateinit var privacyLogger: PrivacyLogger
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var iconContainer: StatusIconContainer
|
private lateinit var iconContainer: StatusIconContainer
|
||||||
@@ -69,6 +75,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
|
|||||||
private lateinit var safetyCenterManager: SafetyCenterManager
|
private lateinit var safetyCenterManager: SafetyCenterManager
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var deviceProvisionedController: DeviceProvisionedController
|
private lateinit var deviceProvisionedController: DeviceProvisionedController
|
||||||
|
@Mock
|
||||||
|
private lateinit var featureFlags: FeatureFlags
|
||||||
|
|
||||||
private val uiExecutor = FakeExecutor(FakeSystemClock())
|
private val uiExecutor = FakeExecutor(FakeSystemClock())
|
||||||
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
|
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
|
||||||
@@ -94,6 +102,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
|
|||||||
uiEventLogger,
|
uiEventLogger,
|
||||||
privacyChip,
|
privacyChip,
|
||||||
privacyDialogController,
|
privacyDialogController,
|
||||||
|
privacyDialogControllerV2,
|
||||||
privacyLogger,
|
privacyLogger,
|
||||||
iconContainer,
|
iconContainer,
|
||||||
permissionManager,
|
permissionManager,
|
||||||
@@ -103,7 +112,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
|
|||||||
appOpsController,
|
appOpsController,
|
||||||
broadcastDispatcher,
|
broadcastDispatcher,
|
||||||
safetyCenterManager,
|
safetyCenterManager,
|
||||||
deviceProvisionedController
|
deviceProvisionedController,
|
||||||
|
featureFlags
|
||||||
)
|
)
|
||||||
|
|
||||||
backgroundExecutor.runAllReady()
|
backgroundExecutor.runAllReady()
|
||||||
@@ -154,17 +164,34 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testPrivacyChipClicked() {
|
fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterDisabled() {
|
||||||
|
whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
|
||||||
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
|
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
|
||||||
controller.onParentVisible()
|
controller.onParentVisible()
|
||||||
val captor = argumentCaptor<View.OnClickListener>()
|
val captor = argumentCaptor<View.OnClickListener>()
|
||||||
verify(privacyChip).setOnClickListener(capture(captor))
|
verify(privacyChip).setOnClickListener(capture(captor))
|
||||||
captor.value.onClick(privacyChip)
|
captor.value.onClick(privacyChip)
|
||||||
verify(privacyDialogController).showDialog(any(Context::class.java))
|
verify(privacyDialogController).showDialog(any(Context::class.java))
|
||||||
|
verify(privacyDialogControllerV2, never())
|
||||||
|
.showDialog(any(Context::class.java), any(View::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSafetyCenterFlag() {
|
fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterDisabled() {
|
||||||
|
whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
|
||||||
|
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
|
||||||
|
controller.onParentVisible()
|
||||||
|
val captor = argumentCaptor<View.OnClickListener>()
|
||||||
|
verify(privacyChip).setOnClickListener(capture(captor))
|
||||||
|
captor.value.onClick(privacyChip)
|
||||||
|
verify(privacyDialogController).showDialog(any(Context::class.java))
|
||||||
|
verify(privacyDialogControllerV2, never())
|
||||||
|
.showDialog(any(Context::class.java), any(View::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterEnabled() {
|
||||||
|
whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
|
||||||
val receiverCaptor = argumentCaptor<BroadcastReceiver>()
|
val receiverCaptor = argumentCaptor<BroadcastReceiver>()
|
||||||
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
|
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
|
||||||
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
|
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
|
||||||
@@ -179,6 +206,28 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
|
|||||||
verify(privacyChip).setOnClickListener(capture(captor))
|
verify(privacyChip).setOnClickListener(capture(captor))
|
||||||
captor.value.onClick(privacyChip)
|
captor.value.onClick(privacyChip)
|
||||||
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
|
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
|
||||||
|
verify(privacyDialogControllerV2, never())
|
||||||
|
.showDialog(any(Context::class.java), any(View::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterEnabled() {
|
||||||
|
whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
|
||||||
|
val receiverCaptor = argumentCaptor<BroadcastReceiver>()
|
||||||
|
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
|
||||||
|
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
|
||||||
|
any(), any(), nullable(), anyInt(), nullable())
|
||||||
|
receiverCaptor.value.onReceive(
|
||||||
|
context,
|
||||||
|
Intent(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED)
|
||||||
|
)
|
||||||
|
backgroundExecutor.runAllReady()
|
||||||
|
controller.onParentVisible()
|
||||||
|
val captor = argumentCaptor<View.OnClickListener>()
|
||||||
|
verify(privacyChip).setOnClickListener(capture(captor))
|
||||||
|
captor.value.onClick(privacyChip)
|
||||||
|
verify(privacyDialogControllerV2).showDialog(any(Context::class.java), eq(privacyChip))
|
||||||
|
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user