Merge "Enable teamfood on the bouncer messages feature" into main
diff --git a/core/java/android/database/sqlite/package.html b/core/java/android/database/sqlite/package.html
index 2c36646..f2df536 100644
--- a/core/java/android/database/sqlite/package.html
+++ b/core/java/android/database/sqlite/package.html
@@ -15,7 +15,7 @@
<a href="{@docRoot}studio/command-line/sqlite3.html">sqlite3</a> command-line
database tool. On your development machine, run the tool from the
<code>platform-tools/</code> folder of your SDK. On the emulator, run the tool
-with adb shell, for example, <code>adb -e shell sqlite3</code>.
+with adb shell, for example, <code>adb shell sqlite3</code>.
<p>The version of SQLite depends on the version of Android. See the following table:
<table style="width:auto;">
@@ -42,15 +42,19 @@
<ul>
<li>If available, use the sqlite3 tool, for example:
- <code>adb -e shell sqlite3 --version</code>.</li>
+ <code>adb shell sqlite3 --version</code>.</li>
<li>Create and query an in-memory database as shown in the following code sample:
<pre>
String query = "select sqlite_version() AS sqlite_version";
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(":memory:", null);
Cursor cursor = db.rawQuery(query, null);
String sqliteVersion = "";
- if (cursor.moveToNext()) {
- sqliteVersion = cursor.getString(0);
+ try {
+ if (cursor.moveToNext()) {
+ sqliteVersion = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
}</pre>
</li>
</ul>
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 46a9d42..d0f2ce8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -48,13 +48,13 @@
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
-import com.android.systemui.res.R
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
@@ -104,7 +104,8 @@
modifier: Modifier = Modifier,
) {
val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
- val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState()
+ val authMethodViewModel: AuthMethodBouncerViewModel? by
+ viewModel.authMethodViewModel.collectAsState()
val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
var dialog: Dialog? by remember { mutableStateOf(null) }
val backgroundColor = MaterialTheme.colorScheme.surface
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 57a4224..4cfc6aa 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -185,9 +185,6 @@
/** Whether the pattern should be visible for the currently-selected user. */
val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible
- /** The minimal length of a pattern. */
- val minPatternLength: Int = repository.minPatternLength
-
private var throttlingCountdownJob: Job? = null
init {
@@ -243,39 +240,46 @@
* Attempts to authenticate the user and unlock the device.
*
* If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
- * supports auto-confirming, and the input's length is at least the code's length. Otherwise,
- * `null` is returned.
+ * supports auto-confirming, and the input's length is at least the required length. Otherwise,
+ * `AuthenticationResult.SKIPPED` is returned.
*
* @param input The input from the user to try to authenticate with. This can be a list of
* different things, based on the current authentication method.
* @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
* request to validate.
- * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
- * authentication failed, `null` if the check was not performed.
+ * @return The result of this authentication attempt.
*/
- suspend fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? {
+ suspend fun authenticate(
+ input: List<Any>,
+ tryAutoConfirm: Boolean = false
+ ): AuthenticationResult {
if (input.isEmpty()) {
throw IllegalArgumentException("Input was empty!")
}
+ val authMethod = getAuthenticationMethod()
val skipCheck =
when {
// We're being throttled, the UI layer should not have called this; skip the
// attempt.
isThrottled.value -> true
+ // The pattern is too short; skip the attempt.
+ authMethod == DomainLayerAuthenticationMethodModel.Pattern &&
+ input.size < repository.minPatternLength -> true
// Auto-confirm attempt when the feature is not enabled; skip the attempt.
tryAutoConfirm && !isAutoConfirmEnabled.value -> true
// Auto-confirm should skip the attempt if the pin entered is too short.
- tryAutoConfirm && input.size < repository.getPinLength() -> true
+ tryAutoConfirm &&
+ authMethod == DomainLayerAuthenticationMethodModel.Pin &&
+ input.size < repository.getPinLength() -> true
else -> false
}
if (skipCheck) {
- return null
+ return AuthenticationResult.SKIPPED
}
// Attempt to authenticate:
- val authMethod = getAuthenticationMethod()
- val credential = authMethod.createCredential(input) ?: return null
+ val credential = authMethod.createCredential(input) ?: return AuthenticationResult.SKIPPED
val authenticationResult = repository.checkCredential(credential)
credential.zeroize()
@@ -299,7 +303,11 @@
refreshThrottling()
}
- return authenticationResult.isSuccessful
+ return if (authenticationResult.isSuccessful) {
+ AuthenticationResult.SUCCEEDED
+ } else {
+ AuthenticationResult.FAILED
+ }
}
/** Starts refreshing the throttling state every second. */
@@ -383,3 +391,16 @@
}
}
}
+
+/** Result of a user authentication attempt. */
+enum class AuthenticationResult {
+ /** Authentication succeeded and the device is now unlocked. */
+ SUCCEEDED,
+ /** Authentication failed and the device remains unlocked. */
+ FAILED,
+ /**
+ * Authentication was not performed, e.g. due to insufficient input, and the device remains
+ * unlocked.
+ */
+ SKIPPED,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 9b2f2ba..bae0ac7 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -17,8 +17,8 @@
package com.android.systemui.bouncer.domain.interactor
import android.content.Context
-import com.android.systemui.res.R
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.bouncer.data.repository.BouncerRepository
@@ -26,6 +26,7 @@
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
@@ -34,6 +35,7 @@
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@@ -92,9 +94,6 @@
/** Whether the pattern should be visible for the currently-selected user. */
val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible
- /** The minimal length of a pattern. */
- val minPatternLength = authenticationInteractor.minPatternLength
-
init {
if (flags.isEnabled()) {
// Clear the message if moved from throttling to no-longer throttling.
@@ -184,33 +183,44 @@
* dismissed and hidden.
*
* If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
- * supports auto-confirming, and the input's length is at least the code's length. Otherwise,
- * `null` is returned.
+ * supports auto-confirming, and the input's length is at least the required length. Otherwise,
+ * `AuthenticationResult.SKIPPED` is returned.
*
* @param input The input from the user to try to authenticate with. This can be a list of
* different things, based on the current authentication method.
* @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
* request to validate.
- * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
- * authentication failed, `null` if the check was not performed.
+ * @return The result of this authentication attempt.
*/
suspend fun authenticate(
input: List<Any>,
tryAutoConfirm: Boolean = false,
- ): Boolean? {
- val isAuthenticated =
- authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null
-
- if (isAuthenticated) {
- sceneInteractor.changeScene(
- scene = SceneModel(SceneKey.Gone),
- loggingReason = "successful authentication",
- )
- } else {
- showErrorMessage()
+ ): AuthenticationResult {
+ if (input.isEmpty()) {
+ return AuthenticationResult.SKIPPED
}
-
- return isAuthenticated
+ // Switching to the application scope here since this method is often called from
+ // view-models, whose lifecycle (and thus scope) is shorter than this interactor.
+ // This allows the task to continue running properly even when the calling scope has been
+ // cancelled.
+ return applicationScope
+ .async {
+ val authResult = authenticationInteractor.authenticate(input, tryAutoConfirm)
+ when (authResult) {
+ // Authentication succeeded.
+ AuthenticationResult.SUCCEEDED ->
+ sceneInteractor.changeScene(
+ scene = SceneModel(SceneKey.Gone),
+ loggingReason = "successful authentication",
+ )
+ // Authentication failed.
+ AuthenticationResult.FAILED -> showErrorMessage()
+ // Authentication skipped.
+ AuthenticationResult.SKIPPED -> if (!tryAutoConfirm) showErrorMessage()
+ }
+ authResult
+ }
+ .await()
}
/**
@@ -221,7 +231,7 @@
* For example, if the user entered a pattern that's too short, the system can show the error
* message without having the attempt trigger throttling.
*/
- suspend fun showErrorMessage() {
+ private suspend fun showErrorMessage() {
repository.setMessage(errorMessage(authenticationInteractor.getAuthenticationMethod()))
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 4546bea..0b0a8f5 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -16,12 +16,21 @@
package com.android.systemui.bouncer.ui.viewmodel
+import android.annotation.StringRes
+import android.util.Log
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
sealed class AuthMethodBouncerViewModel(
+ protected val viewModelScope: CoroutineScope,
+ protected val interactor: BouncerInteractor,
+
/**
* Whether user input is enabled.
*
@@ -29,7 +38,6 @@
* being able to attempt to unlock the device.
*/
val isInputEnabled: StateFlow<Boolean>,
- private val interactor: BouncerInteractor,
) {
private val _animateFailure = MutableStateFlow(false)
@@ -42,12 +50,26 @@
/** Whether the input method editor (for example, the software keyboard) is visible. */
private var isImeVisible: Boolean = false
+ /** The authentication method that corresponds to this view model. */
+ abstract val authenticationMethod: AuthenticationMethodModel
+
/**
- * Notifies that the failure animation has been shown. This should be called to consume a `true`
- * value in [animateFailure].
+ * String resource ID of the failure message to be shown during throttling.
+ *
+ * The message must include 2 number parameters: the first one indicating how many unsuccessful
+ * attempts were made, and the second one indicating in how many seconds throttling will expire.
*/
- fun onFailureAnimationShown() {
- _animateFailure.value = false
+ @get:StringRes abstract val throttlingMessageId: Int
+
+ /** Notifies that the UI has been shown to the user. */
+ fun onShown() {
+ clearInput()
+ interactor.resetMessage()
+ }
+
+ /** Notifies that the user has placed down a pointer. */
+ fun onDown() {
+ interactor.onDown()
}
/**
@@ -65,8 +87,38 @@
isImeVisible = isVisible
}
- /** Ask the UI to show the failure animation. */
- protected fun showFailureAnimation() {
- _animateFailure.value = true
+ /**
+ * Notifies that the failure animation has been shown. This should be called to consume a `true`
+ * value in [animateFailure].
+ */
+ fun onFailureAnimationShown() {
+ _animateFailure.value = false
+ }
+
+ /** Clears any previously-entered input. */
+ protected abstract fun clearInput()
+
+ /** Returns the input entered so far. */
+ protected abstract fun getInput(): List<Any>
+
+ /**
+ * Attempts to authenticate the user using the current input value.
+ *
+ * @see BouncerInteractor.authenticate
+ */
+ protected fun tryAuthenticate(useAutoConfirm: Boolean = false) {
+ viewModelScope.launch {
+ Log.d("Danny", "tryAuthenticate(useAutoConfirm=$useAutoConfirm)")
+ val authenticationResult = interactor.authenticate(getInput(), useAutoConfirm)
+ Log.d("Danny", "result = $authenticationResult")
+ if (authenticationResult == AuthenticationResult.SKIPPED && useAutoConfirm) {
+ return@launch
+ }
+ _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
+
+ // TODO(b/291528545): On success, this should only be cleared after the view is animated
+ // away).
+ clearInput()
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 15d1dae..782ead3 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -17,16 +17,19 @@
package com.android.systemui.bouncer.ui.viewmodel
import android.content.Context
-import com.android.systemui.res.R
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import javax.inject.Inject
import kotlin.math.ceil
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -35,6 +38,7 @@
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.job
import kotlinx.coroutines.launch
/** Holds UI state and handles user input on bouncer UIs. */
@@ -44,8 +48,9 @@
constructor(
@Application private val applicationContext: Context,
@Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
private val bouncerInteractor: BouncerInteractor,
- private val authenticationInteractor: AuthenticationInteractor,
+ authenticationInteractor: AuthenticationInteractor,
flags: SceneContainerFlags,
) {
private val isInputEnabled: StateFlow<Boolean> =
@@ -57,91 +62,45 @@
initialValue = !bouncerInteractor.isThrottled.value,
)
- private val pin: PinBouncerViewModel by lazy {
- PinBouncerViewModel(
- applicationContext = applicationContext,
- applicationScope = applicationScope,
- interactor = bouncerInteractor,
- isInputEnabled = isInputEnabled,
- )
- }
-
- private val password: PasswordBouncerViewModel by lazy {
- PasswordBouncerViewModel(
- applicationScope = applicationScope,
- interactor = bouncerInteractor,
- isInputEnabled = isInputEnabled,
- )
- }
-
- private val pattern: PatternBouncerViewModel by lazy {
- PatternBouncerViewModel(
- applicationContext = applicationContext,
- applicationScope = applicationScope,
- interactor = bouncerInteractor,
- isInputEnabled = isInputEnabled,
- )
- }
-
/** View-model for the current UI, based on the current authentication method. */
- val authMethod: StateFlow<AuthMethodBouncerViewModel?> =
+ val authMethodViewModel: StateFlow<AuthMethodBouncerViewModel?> =
authenticationInteractor.authenticationMethod
- .map { authenticationMethod ->
- when (authenticationMethod) {
- is AuthenticationMethodModel.Pin -> pin
- is AuthenticationMethodModel.Password -> password
- is AuthenticationMethodModel.Pattern -> pattern
- else -> null
- }
- }
+ .map(::getChildViewModel)
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null,
)
+ // Handle to the scope of the child ViewModel (stored in [authMethod]).
+ private var childViewModelScope: CoroutineScope? = null
+
init {
if (flags.isEnabled()) {
applicationScope.launch {
- bouncerInteractor.isThrottled
- .map { isThrottled ->
- if (isThrottled) {
- when (authenticationInteractor.getAuthenticationMethod()) {
- is AuthenticationMethodModel.Pin ->
- R.string.kg_too_many_failed_pin_attempts_dialog_message
- is AuthenticationMethodModel.Password ->
- R.string.kg_too_many_failed_password_attempts_dialog_message
- is AuthenticationMethodModel.Pattern ->
- R.string.kg_too_many_failed_pattern_attempts_dialog_message
- else -> null
- }?.let { stringResourceId ->
- applicationContext.getString(
- stringResourceId,
- bouncerInteractor.throttling.value.failedAttemptCount,
- ceil(bouncerInteractor.throttling.value.remainingMs / 1000f)
- .toInt(),
- )
- }
+ combine(bouncerInteractor.isThrottled, authMethodViewModel) {
+ isThrottled,
+ authMethodViewModel ->
+ if (isThrottled && authMethodViewModel != null) {
+ applicationContext.getString(
+ authMethodViewModel.throttlingMessageId,
+ bouncerInteractor.throttling.value.failedAttemptCount,
+ ceil(bouncerInteractor.throttling.value.remainingMs / 1000f)
+ .toInt(),
+ )
} else {
null
}
}
.distinctUntilChanged()
- .collect { dialogMessageOrNull ->
- if (dialogMessageOrNull != null) {
- _throttlingDialogMessage.value = dialogMessageOrNull
- }
- }
+ .collect { dialogMessage -> _throttlingDialogMessage.value = dialogMessage }
}
}
}
/** The user-facing message to show in the bouncer. */
val message: StateFlow<MessageViewModel> =
- combine(
- bouncerInteractor.message,
- bouncerInteractor.isThrottled,
- ) { message, isThrottled ->
+ combine(bouncerInteractor.message, bouncerInteractor.isThrottled) { message, isThrottled ->
toMessageViewModel(message, isThrottled)
}
.stateIn(
@@ -186,6 +145,50 @@
)
}
+ private fun getChildViewModel(
+ authenticationMethod: AuthenticationMethodModel,
+ ): AuthMethodBouncerViewModel? {
+ // If the current child view-model matches the authentication method, reuse it instead of
+ // creating a new instance.
+ val childViewModel = authMethodViewModel.value
+ if (authenticationMethod == childViewModel?.authenticationMethod) {
+ return childViewModel
+ }
+
+ childViewModelScope?.cancel()
+ val newViewModelScope = createChildCoroutineScope(applicationScope)
+ childViewModelScope = newViewModelScope
+ return when (authenticationMethod) {
+ is AuthenticationMethodModel.Pin ->
+ PinBouncerViewModel(
+ applicationContext = applicationContext,
+ viewModelScope = newViewModelScope,
+ interactor = bouncerInteractor,
+ isInputEnabled = isInputEnabled,
+ )
+ is AuthenticationMethodModel.Password ->
+ PasswordBouncerViewModel(
+ viewModelScope = newViewModelScope,
+ interactor = bouncerInteractor,
+ isInputEnabled = isInputEnabled,
+ )
+ is AuthenticationMethodModel.Pattern ->
+ PatternBouncerViewModel(
+ applicationContext = applicationContext,
+ viewModelScope = newViewModelScope,
+ interactor = bouncerInteractor,
+ isInputEnabled = isInputEnabled,
+ )
+ else -> null
+ }
+ }
+
+ private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope {
+ return CoroutineScope(
+ SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher
+ )
+ }
+
data class MessageViewModel(
val text: String,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 9e10f29..fe77419 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -16,22 +16,24 @@
package com.android.systemui.bouncer.ui.viewmodel
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.res.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.launch
/** Holds UI state and handles user input for the password bouncer UI. */
class PasswordBouncerViewModel(
- private val applicationScope: CoroutineScope,
- private val interactor: BouncerInteractor,
+ viewModelScope: CoroutineScope,
+ interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>,
) :
AuthMethodBouncerViewModel(
- isInputEnabled = isInputEnabled,
+ viewModelScope = viewModelScope,
interactor = interactor,
+ isInputEnabled = isInputEnabled,
) {
private val _password = MutableStateFlow("")
@@ -39,10 +41,16 @@
/** The password entered so far. */
val password: StateFlow<String> = _password.asStateFlow()
- /** Notifies that the UI has been shown to the user. */
- fun onShown() {
+ override val authenticationMethod = AuthenticationMethodModel.Password
+
+ override val throttlingMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message
+
+ override fun clearInput() {
_password.value = ""
- interactor.resetMessage()
+ }
+
+ override fun getInput(): List<Any> {
+ return _password.value.toCharArray().toList()
}
/** Notifies that the user has changed the password input. */
@@ -60,17 +68,8 @@
/** Notifies that the user has pressed the key for attempting to authenticate the password. */
fun onAuthenticateKeyPressed() {
- val password = _password.value.toCharArray().toList()
- if (password.isEmpty()) {
- return
- }
-
- _password.value = ""
-
- applicationScope.launch {
- if (interactor.authenticate(password) != true) {
- showFailureAnimation()
- }
+ if (_password.value.isNotEmpty()) {
+ tryAuthenticate()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 497276b..52adf54 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -18,8 +18,10 @@
import android.content.Context
import android.util.TypedValue
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.res.R
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
@@ -31,18 +33,18 @@
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
/** Holds UI state and handles user input for the pattern bouncer UI. */
class PatternBouncerViewModel(
private val applicationContext: Context,
- private val applicationScope: CoroutineScope,
- private val interactor: BouncerInteractor,
+ viewModelScope: CoroutineScope,
+ interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>,
) :
AuthMethodBouncerViewModel(
- isInputEnabled = isInputEnabled,
+ viewModelScope = viewModelScope,
interactor = interactor,
+ isInputEnabled = isInputEnabled,
) {
/** The number of columns in the dot grid. */
@@ -58,7 +60,7 @@
_selectedDots
.map { it.toList() }
.stateIn(
- scope = applicationScope,
+ scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = emptyList(),
)
@@ -76,15 +78,9 @@
/** Whether the pattern itself should be rendered visibly. */
val isPatternVisible: StateFlow<Boolean> = interactor.isPatternVisible
- /** Notifies that the UI has been shown to the user. */
- fun onShown() {
- interactor.resetMessage()
- }
+ override val authenticationMethod = AuthenticationMethodModel.Pattern
- /** Notifies that the user has placed down a pointer, not necessarily dragging just yet. */
- fun onDown() {
- interactor.onDown()
- }
+ override val throttlingMessageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message
/** Notifies that the user has started a drag gesture across the dot grid. */
fun onDragStart() {
@@ -164,24 +160,23 @@
/** Notifies that the user has ended the drag gesture across the dot grid. */
fun onDragEnd() {
- val pattern = _selectedDots.value.map { it.toCoordinate() }
-
+ val pattern = getInput()
if (pattern.size == 1) {
// Single dot patterns are treated as erroneous/false taps:
interactor.onFalseUserInput()
}
+ tryAuthenticate()
+ }
+
+ override fun clearInput() {
_dots.value = defaultDots()
_currentDot.value = null
_selectedDots.value = linkedSetOf()
+ }
- applicationScope.launch {
- if (pattern.size < interactor.minPatternLength) {
- interactor.showErrorMessage()
- } else if (interactor.authenticate(pattern) != true) {
- showFailureAnimation()
- }
- }
+ override fun getInput(): List<Any> {
+ return _selectedDots.value.map(PatternDotViewModel::toCoordinate)
}
private fun defaultDots(): List<PatternDotViewModel> {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 8e6421e..b90e255 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -18,7 +18,9 @@
import android.content.Context
import com.android.keyguard.PinShapeAdapter
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.res.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -26,18 +28,18 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
/** Holds UI state and handles user input for the PIN code bouncer UI. */
class PinBouncerViewModel(
applicationContext: Context,
- private val applicationScope: CoroutineScope,
- private val interactor: BouncerInteractor,
+ viewModelScope: CoroutineScope,
+ interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>,
) :
AuthMethodBouncerViewModel(
- isInputEnabled = isInputEnabled,
+ viewModelScope = viewModelScope,
interactor = interactor,
+ isInputEnabled = isInputEnabled,
) {
val pinShapes = PinShapeAdapter(applicationContext)
@@ -61,7 +63,7 @@
)
}
.stateIn(
- scope = applicationScope,
+ scope = viewModelScope,
// Make sure this is kept as WhileSubscribed or we can run into a bug where the
// downstream continues to receive old/stale/cached values.
started = SharingStarted.WhileSubscribed(),
@@ -73,21 +75,14 @@
interactor.isAutoConfirmEnabled
.map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
.stateIn(
- scope = applicationScope,
+ scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = ActionButtonAppearance.Hidden,
)
- /** Notifies that the UI has been shown to the user. */
- fun onShown() {
- clearPinInput()
- interactor.resetMessage()
- }
+ override val authenticationMethod = AuthenticationMethodModel.Pin
- /** Notifies that the user has placed down a pointer. */
- fun onDown() {
- interactor.onDown()
- }
+ override val throttlingMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
/** Notifies that the user clicked on a PIN button with the given digit value. */
fun onPinButtonClicked(input: Int) {
@@ -109,7 +104,8 @@
/** Notifies that the user long-pressed the backspace button. */
fun onBackspaceButtonLongPressed() {
- clearPinInput()
+ clearInput()
+ interactor.clearMessage()
}
/** Notifies that the user clicked the "enter" button. */
@@ -117,24 +113,12 @@
tryAuthenticate(useAutoConfirm = false)
}
- private fun clearPinInput() {
+ override fun clearInput() {
mutablePinInput.value = mutablePinInput.value.clearAll()
}
- private fun tryAuthenticate(useAutoConfirm: Boolean) {
- val pinCode = mutablePinInput.value.getPin()
-
- applicationScope.launch {
- val isSuccess = interactor.authenticate(pinCode, useAutoConfirm) ?: return@launch
-
- if (!isSuccess) {
- showFailureAnimation()
- }
-
- // TODO(b/291528545): this should not be cleared on success (at least until the view
- // is animated away).
- clearPinInput()
- }
+ override fun getInput(): List<Any> {
+ return mutablePinInput.value.getPin()
}
private fun computeBackspaceButtonAppearance(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index fc7d20a..874053a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -236,7 +236,8 @@
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
+ assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(isThrottled).isFalse()
}
@@ -246,7 +247,8 @@
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
- assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4))).isFalse()
+ assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4)))
+ .isEqualTo(AuthenticationResult.FAILED)
}
@Test(expected = IllegalArgumentException::class)
@@ -267,7 +269,7 @@
overrideCredential(pin)
}
- assertThat(underTest.authenticate(pin)).isTrue()
+ assertThat(underTest.authenticate(pin)).isEqualTo(AuthenticationResult.SUCCEEDED)
}
@Test
@@ -282,7 +284,8 @@
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
- assertThat(underTest.authenticate(List(17) { 9 })).isFalse()
+ assertThat(underTest.authenticate(List(17) { 9 }))
+ .isEqualTo(AuthenticationResult.FAILED)
}
@Test
@@ -293,7 +296,8 @@
DataLayerAuthenticationMethodModel.Password
)
- assertThat(underTest.authenticate("password".toList())).isTrue()
+ assertThat(underTest.authenticate("password".toList()))
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(isThrottled).isFalse()
}
@@ -304,7 +308,8 @@
DataLayerAuthenticationMethodModel.Password
)
- assertThat(underTest.authenticate("alohomora".toList())).isFalse()
+ assertThat(underTest.authenticate("alohomora".toList()))
+ .isEqualTo(AuthenticationResult.FAILED)
}
@Test
@@ -314,7 +319,8 @@
DataLayerAuthenticationMethodModel.Pattern
)
- assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)).isTrue()
+ assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
}
@Test
@@ -327,22 +333,14 @@
assertThat(
underTest.authenticate(
listOf(
- AuthenticationPatternCoordinate(
- x = 2,
- y = 0,
- ),
- AuthenticationPatternCoordinate(
- x = 2,
- y = 1,
- ),
- AuthenticationPatternCoordinate(
- x = 2,
- y = 2,
- ),
+ AuthenticationPatternCoordinate(x = 2, y = 0),
+ AuthenticationPatternCoordinate(x = 2, y = 1),
+ AuthenticationPatternCoordinate(x = 2, y = 2),
+ AuthenticationPatternCoordinate(x = 1, y = 2),
)
)
)
- .isFalse()
+ .isEqualTo(AuthenticationResult.FAILED)
}
@Test
@@ -361,7 +359,7 @@
tryAutoConfirm = true
)
)
- .isNull()
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(isThrottled).isFalse()
}
@@ -379,7 +377,7 @@
tryAutoConfirm = true
)
)
- .isFalse()
+ .isEqualTo(AuthenticationResult.FAILED)
assertThat(isUnlocked).isFalse()
}
@@ -397,7 +395,7 @@
tryAutoConfirm = true
)
)
- .isFalse()
+ .isEqualTo(AuthenticationResult.FAILED)
assertThat(isUnlocked).isFalse()
}
@@ -415,7 +413,7 @@
tryAutoConfirm = true
)
)
- .isTrue()
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(isUnlocked).isTrue()
}
@@ -433,7 +431,7 @@
tryAutoConfirm = true
)
)
- .isNull()
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(isUnlocked).isFalse()
}
@@ -445,7 +443,8 @@
DataLayerAuthenticationMethodModel.Password
)
- assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true)).isNull()
+ assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true))
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(isUnlocked).isFalse()
}
@@ -490,7 +489,8 @@
)
// Correct PIN, but throttled, so doesn't attempt it:
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isNull()
+ assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(isUnlocked).isFalse()
assertThat(isThrottled).isTrue()
assertThat(throttling)
@@ -536,7 +536,8 @@
)
// Correct PIN and no longer throttled so unlocks successfully:
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
+ assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(isUnlocked).isTrue()
assertThat(isThrottled).isFalse()
assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 77d8102..c0257df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -17,13 +17,14 @@
package com.android.systemui.bouncer.domain.interactor
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.AuthenticationResult
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
@@ -87,7 +88,8 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
// Wrong input.
- assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
+ assertThat(underTest.authenticate(listOf(9, 8, 7)))
+ .isEqualTo(AuthenticationResult.FAILED)
assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -95,7 +97,8 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
// Correct input.
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
+ assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@@ -115,13 +118,14 @@
underTest.clearMessage()
// Incomplete input.
- assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)).isNull()
+ assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(message).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
// Wrong 6-digit pin
assertThat(underTest.authenticate(listOf(1, 2, 3, 5, 5, 6), tryAutoConfirm = true))
- .isFalse()
+ .isEqualTo(AuthenticationResult.FAILED)
assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -132,7 +136,7 @@
tryAutoConfirm = true
)
)
- .isTrue()
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@@ -150,7 +154,8 @@
underTest.clearMessage()
// Incomplete input.
- assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)).isNull()
+ assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(message).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -161,7 +166,7 @@
tryAutoConfirm = true
)
)
- .isNull()
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(message).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -187,7 +192,8 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
// Wrong input.
- assertThat(underTest.authenticate("alohamora".toList())).isFalse()
+ assertThat(underTest.authenticate("alohamora".toList()))
+ .isEqualTo(AuthenticationResult.FAILED)
assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -195,7 +201,8 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
// Correct input.
- assertThat(underTest.authenticate("password".toList())).isTrue()
+ assertThat(underTest.authenticate("password".toList()))
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@@ -220,8 +227,30 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Wrong input.
- assertThat(underTest.authenticate(listOf(AuthenticationPatternCoordinate(1, 2))))
- .isFalse()
+ val wrongPattern =
+ listOf(
+ AuthenticationPatternCoordinate(1, 2),
+ AuthenticationPatternCoordinate(1, 1),
+ AuthenticationPatternCoordinate(0, 0),
+ AuthenticationPatternCoordinate(0, 1),
+ )
+ assertThat(wrongPattern).isNotEqualTo(FakeAuthenticationRepository.PATTERN)
+ assertThat(wrongPattern.size).isAtLeast(utils.authenticationRepository.minPatternLength)
+ assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED)
+ assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
+ assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+
+ underTest.resetMessage()
+ assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
+
+ // Too short input.
+ val tooShortPattern =
+ FakeAuthenticationRepository.PATTERN.subList(
+ 0,
+ utils.authenticationRepository.minPatternLength - 1
+ )
+ assertThat(underTest.authenticate(tooShortPattern))
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -229,7 +258,8 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Correct input.
- assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)).isTrue()
+ assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@@ -294,7 +324,8 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) { times ->
// Wrong PIN.
- assertThat(underTest.authenticate(listOf(6, 7, 8, 9))).isFalse()
+ assertThat(underTest.authenticate(listOf(6, 7, 8, 9)))
+ .isEqualTo(AuthenticationResult.FAILED)
if (
times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING - 1
) {
@@ -317,7 +348,8 @@
)
// Correct PIN, but throttled, so doesn't change away from the bouncer scene:
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isNull()
+ assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+ .isEqualTo(AuthenticationResult.SKIPPED)
assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
assertTryAgainMessage(
message,
@@ -347,7 +379,8 @@
assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer)
// Correct PIN and no longer throttled so changes to the Gone scene:
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
+ assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
+ .isEqualTo(AuthenticationResult.SUCCEEDED)
assertThat(currentScene?.key).isEqualTo(SceneKey.Gone)
assertThat(isThrottled).isFalse()
assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index 9011c2f..2f7dde0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -45,7 +45,7 @@
private val underTest =
PinBouncerViewModel(
applicationContext = context,
- applicationScope = testScope.backgroundScope,
+ viewModelScope = testScope.backgroundScope,
interactor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 2c96bcc..da2534d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -46,7 +46,7 @@
private val testScope = utils.testScope
private val authenticationInteractor =
utils.authenticationInteractor(
- repository = utils.authenticationRepository(),
+ repository = utils.authenticationRepository,
)
private val bouncerInteractor =
utils.bouncerInteractor(
@@ -66,7 +66,8 @@
authMethodsToTest().forEach { authMethod ->
utils.authenticationRepository.setAuthenticationMethod(authMethod)
- val job = underTest.authMethod.onEach { authMethodViewModel = it }.launchIn(this)
+ val job =
+ underTest.authMethodViewModel.onEach { authMethodViewModel = it }.launchIn(this)
runCurrent()
if (authMethod.isSecure) {
@@ -86,22 +87,43 @@
}
@Test
- fun authMethod_reusesInstances() =
+ fun authMethodChanged_doesNotReuseInstances() =
testScope.runTest {
val seen =
mutableMapOf<DomainLayerAuthenticationMethodModel, AuthMethodBouncerViewModel>()
val authMethodViewModel: AuthMethodBouncerViewModel? by
- collectLastValue(underTest.authMethod)
+ collectLastValue(underTest.authMethodViewModel)
+
// First pass, populate our "seen" map:
authMethodsToTest().forEach { authMethod ->
utils.authenticationRepository.setAuthenticationMethod(authMethod)
authMethodViewModel?.let { seen[authMethod] = it }
}
- // Second pass, assert same instances are reused:
+ // Second pass, assert same instances are not reused:
authMethodsToTest().forEach { authMethod ->
utils.authenticationRepository.setAuthenticationMethod(authMethod)
- authMethodViewModel?.let { assertThat(it).isSameInstanceAs(seen[authMethod]) }
+ authMethodViewModel?.let {
+ assertThat(it.authenticationMethod).isEqualTo(authMethod)
+ assertThat(it).isNotSameInstanceAs(seen[authMethod])
+ }
+ }
+ }
+
+ @Test
+ fun authMethodUnchanged_reusesInstances() =
+ testScope.runTest {
+ authMethodsToTest().forEach { authMethod ->
+ utils.authenticationRepository.setAuthenticationMethod(authMethod)
+ val firstInstance: AuthMethodBouncerViewModel? =
+ collectLastValue(underTest.authMethodViewModel).invoke()
+
+ utils.authenticationRepository.setAuthenticationMethod(authMethod)
+ val secondInstance: AuthMethodBouncerViewModel? =
+ collectLastValue(underTest.authMethodViewModel).invoke()
+
+ firstInstance?.let { assertThat(it.authenticationMethod).isEqualTo(authMethod) }
+ assertThat(secondInstance).isSameInstanceAs(firstInstance)
}
}
@@ -136,7 +158,7 @@
testScope.runTest {
val isInputEnabled by
collectLastValue(
- underTest.authMethod.flatMapLatest { authViewModel ->
+ underTest.authMethodViewModel.flatMapLatest { authViewModel ->
authViewModel?.isInputEnabled ?: emptyFlow()
}
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 3375184..c1b3354 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -17,10 +17,11 @@
package com.android.systemui.bouncer.ui.viewmodel
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
@@ -28,6 +29,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -59,7 +61,7 @@
)
private val underTest =
PasswordBouncerViewModel(
- applicationScope = testScope.backgroundScope,
+ viewModelScope = testScope.backgroundScope,
interactor = bouncerInteractor,
isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@@ -76,19 +78,13 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
-
- underTest.onShown()
+ lockDeviceAndOpenPasswordBouncer()
assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
assertThat(password).isEqualTo("")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+ assertThat(underTest.authenticationMethod)
+ .isEqualTo(DomainAuthenticationMethodModel.Password)
}
@Test
@@ -97,15 +93,7 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- runCurrent()
+ lockDeviceAndOpenPasswordBouncer()
underTest.onPasswordInputChanged("password")
@@ -118,16 +106,9 @@
fun onAuthenticateKeyPressed_whenCorrect() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.desiredScene)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- underTest.onPasswordInputChanged("password")
+ lockDeviceAndOpenPasswordBouncer()
+ underTest.onPasswordInputChanged("password")
underTest.onAuthenticateKeyPressed()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
@@ -139,16 +120,9 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- underTest.onPasswordInputChanged("wrong")
+ lockDeviceAndOpenPasswordBouncer()
+ underTest.onPasswordInputChanged("wrong")
underTest.onAuthenticateKeyPressed()
assertThat(password).isEqualTo("")
@@ -185,14 +159,9 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
+ lockDeviceAndOpenPasswordBouncer()
+
+ // Enter the wrong password:
underTest.onPasswordInputChanged("wrong")
underTest.onAuthenticateKeyPressed()
assertThat(password).isEqualTo("")
@@ -213,14 +182,7 @@
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val password by collectLastValue(underTest.password)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Password
- )
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
+ lockDeviceAndOpenPasswordBouncer()
// The user types a password.
underTest.onPasswordInputChanged("password")
@@ -243,6 +205,18 @@
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
+ private fun TestScope.lockDeviceAndOpenPasswordBouncer() {
+ utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Password)
+ utils.authenticationRepository.setUnlocked(false)
+ sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
+ sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+
+ assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
+ .isEqualTo(SceneModel(SceneKey.Bouncer))
+ underTest.onShown()
+ runCurrent()
+ }
+
companion object {
private const val ENTER_YOUR_PASSWORD = "Enter your password"
private const val WRONG_PASSWORD = "Wrong password"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 102cfe2..bf109d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -17,12 +17,13 @@
package com.android.systemui.bouncer.ui.viewmodel
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
@@ -64,7 +65,7 @@
private val underTest =
PatternBouncerViewModel(
applicationContext = context,
- applicationScope = testScope.backgroundScope,
+ viewModelScope = testScope.backgroundScope,
interactor = bouncerInteractor,
isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@@ -85,14 +86,14 @@
val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
- transitionToPatternBouncer()
-
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+ assertThat(underTest.authenticationMethod)
+ .isEqualTo(DomainAuthenticationMethodModel.Pattern)
}
@Test
@@ -102,9 +103,7 @@
val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
- transitionToPatternBouncer()
- underTest.onShown()
- runCurrent()
+ lockDeviceAndOpenPatternBouncer()
underTest.onDragStart()
@@ -120,8 +119,7 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
- transitionToPatternBouncer()
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
underTest.onDragStart()
assertThat(currentDot).isNull()
CORRECT_PATTERN.forEachIndexed { index, coordinate ->
@@ -158,8 +156,7 @@
val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
- transitionToPatternBouncer()
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
underTest.onDragStart()
CORRECT_PATTERN.subList(0, 3).forEach { coordinate -> dragToCoordinate(coordinate) }
@@ -175,8 +172,7 @@
fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheSameRow() =
testScope.runTest {
val selectedDots by collectLastValue(underTest.selectedDots)
- transitionToPatternBouncer()
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
/*
* Pattern setup, coordinates are (column, row)
@@ -202,8 +198,7 @@
fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheSameColumn() =
testScope.runTest {
val selectedDots by collectLastValue(underTest.selectedDots)
- transitionToPatternBouncer()
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
/*
* Pattern setup, coordinates are (column, row)
@@ -229,8 +224,7 @@
fun onDrag_shouldIncludeDotsThatWereSkippedOverAlongTheDiagonal() =
testScope.runTest {
val selectedDots by collectLastValue(underTest.selectedDots)
- transitionToPatternBouncer()
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
/*
* Pattern setup
@@ -258,8 +252,7 @@
fun onDrag_shouldNotIncludeDotIfItIsNotOnTheLine() =
testScope.runTest {
val selectedDots by collectLastValue(underTest.selectedDots)
- transitionToPatternBouncer()
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
/*
* Pattern setup
@@ -287,8 +280,7 @@
fun onDrag_shouldNotIncludeSkippedOverDotsIfTheyAreAlreadySelected() =
testScope.runTest {
val selectedDots by collectLastValue(underTest.selectedDots)
- transitionToPatternBouncer()
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
/*
* Pattern setup
@@ -315,20 +307,10 @@
@Test
fun onDragEnd_whenPatternTooShort() =
testScope.runTest {
- val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
- val selectedDots by collectLastValue(underTest.selectedDots)
- val currentDot by collectLastValue(underTest.currentDot)
val throttlingDialogMessage by
collectLastValue(bouncerViewModel.throttlingDialogMessage)
- utils.authenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pattern
- )
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
// Enter a pattern that's too short more than enough times that would normally trigger
// throttling if the pattern were not too short and wrong:
@@ -337,7 +319,7 @@
underTest.onDragStart()
CORRECT_PATTERN.subList(
0,
- authenticationInteractor.minPatternLength - 1,
+ utils.authenticationRepository.minPatternLength - 1,
)
.forEach { coordinate ->
underTest.onDrag(
@@ -362,10 +344,10 @@
val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
- transitionToPatternBouncer()
- underTest.onShown()
+ lockDeviceAndOpenPatternBouncer()
+
underTest.onDragStart()
- CORRECT_PATTERN.subList(2, 7).forEach { coordinate -> dragToCoordinate(coordinate) }
+ CORRECT_PATTERN.subList(2, 7).forEach(::dragToCoordinate)
underTest.onDragEnd()
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
@@ -373,7 +355,7 @@
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
// Enter the correct pattern:
- CORRECT_PATTERN.forEach { coordinate -> dragToCoordinate(coordinate) }
+ CORRECT_PATTERN.forEach(::dragToCoordinate)
underTest.onDragEnd()
@@ -382,7 +364,7 @@
private fun dragOverCoordinates(vararg coordinatesDragged: Point) {
underTest.onDragStart()
- coordinatesDragged.forEach { dragToCoordinate(it) }
+ coordinatesDragged.forEach(::dragToCoordinate)
}
private fun dragToCoordinate(coordinate: Point) {
@@ -394,13 +376,15 @@
)
}
- private fun TestScope.transitionToPatternBouncer() {
+ private fun TestScope.lockDeviceAndOpenPatternBouncer() {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
.isEqualTo(SceneModel(SceneKey.Bouncer))
+ underTest.onShown()
+ runCurrent()
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 35238ce..2576204 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -17,11 +17,12 @@
package com.android.systemui.bouncer.ui.viewmodel
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainAuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.res.R
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
@@ -30,6 +31,7 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -62,7 +64,7 @@
private val underTest =
PinBouncerViewModel(
applicationContext = context,
- applicationScope = testScope.backgroundScope,
+ viewModelScope = testScope.backgroundScope,
interactor = bouncerInteractor,
isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@@ -90,6 +92,8 @@
assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
+ assertThat(underTest.authenticationMethod)
+ .isEqualTo(DomainAuthenticationMethodModel.Pin)
}
@Test
@@ -120,14 +124,8 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+ lockDeviceAndOpenPinBouncer()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- runCurrent()
underTest.onPinButtonClicked(1)
assertThat(pin).hasSize(1)
@@ -141,15 +139,8 @@
@Test
fun onPinEdit() =
testScope.runTest {
- val currentScene by collectLastValue(sceneInteractor.desiredScene)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
+ lockDeviceAndOpenPinBouncer()
underTest.onPinButtonClicked(1)
underTest.onPinButtonClicked(2)
@@ -168,18 +159,13 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+ lockDeviceAndOpenPinBouncer()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
- runCurrent()
underTest.onPinButtonClicked(1)
underTest.onPinButtonClicked(2)
underTest.onPinButtonClicked(3)
underTest.onPinButtonClicked(4)
+ runCurrent()
underTest.onBackspaceButtonLongPressed()
@@ -192,13 +178,8 @@
fun onAuthenticateButtonClicked_whenCorrect() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.desiredScene)
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+ lockDeviceAndOpenPinBouncer()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
underTest.onPinButtonClicked(digit)
}
@@ -214,13 +195,8 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+ lockDeviceAndOpenPinBouncer()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
underTest.onPinButtonClicked(1)
underTest.onPinButtonClicked(2)
underTest.onPinButtonClicked(3)
@@ -240,13 +216,8 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+ lockDeviceAndOpenPinBouncer()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
underTest.onPinButtonClicked(1)
underTest.onPinButtonClicked(2)
underTest.onPinButtonClicked(3)
@@ -272,14 +243,9 @@
fun onAutoConfirm_whenCorrect() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.desiredScene)
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAutoConfirmEnabled(true)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+ lockDeviceAndOpenPinBouncer()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
underTest.onPinButtonClicked(digit)
}
@@ -293,14 +259,9 @@
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAutoConfirmEnabled(true)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+ lockDeviceAndOpenPinBouncer()
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit ->
underTest.onPinButtonClicked(digit)
}
@@ -318,13 +279,7 @@
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.desiredScene)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
- utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
- utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
- sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
-
- assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
- underTest.onShown()
+ lockDeviceAndOpenPinBouncer()
// The user types a PIN.
FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
@@ -401,6 +356,18 @@
assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden)
}
+ private fun TestScope.lockDeviceAndOpenPinBouncer() {
+ utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ utils.authenticationRepository.setUnlocked(false)
+ sceneInteractor.changeScene(SceneModel(SceneKey.Bouncer), "reason")
+ sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer), "reason")
+
+ assertThat(collectLastValue(sceneInteractor.desiredScene).invoke())
+ .isEqualTo(SceneModel(SceneKey.Bouncer))
+ underTest.onShown()
+ runCurrent()
+ }
+
companion object {
private const val ENTER_YOUR_PIN = "Enter your pin"
private const val WRONG_PIN = "Wrong pin"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 6b918c6..85bd92b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -76,6 +76,7 @@
* being used when the state is as required (e.g. cannot unlock an already unlocked device, cannot
* put to sleep a device that's already asleep, etc.).
*/
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class SceneFrameworkIntegrationTest : SysuiTestCase() {
@@ -481,7 +482,7 @@
bouncerSceneJob =
if (to.key == SceneKey.Bouncer) {
testScope.backgroundScope.launch {
- bouncerViewModel.authMethod.collect {
+ bouncerViewModel.authMethodViewModel.collect {
// Do nothing. Need this to turn this otherwise cold flow, hot.
}
}
@@ -556,7 +557,7 @@
assertWithMessage("Cannot enter PIN when not on the Bouncer scene!")
.that(getCurrentSceneInUi())
.isEqualTo(SceneKey.Bouncer)
- val authMethodViewModel by collectLastValue(bouncerViewModel.authMethod)
+ val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel)
assertWithMessage("Cannot enter PIN when not using a PIN authentication method!")
.that(authMethodViewModel)
.isInstanceOf(PinBouncerViewModel::class.java)
@@ -613,11 +614,12 @@
private fun TestScope.dismissIme(
showImeBeforeDismissing: Boolean = true,
) {
- if (showImeBeforeDismissing) {
- bouncerViewModel.authMethod.value?.onImeVisibilityChanged(true)
+ bouncerViewModel.authMethodViewModel.value?.apply {
+ if (showImeBeforeDismissing) {
+ onImeVisibilityChanged(true)
+ }
+ onImeVisibilityChanged(false)
+ runCurrent()
}
-
- bouncerViewModel.authMethod.value?.onImeVisibilityChanged(false)
- runCurrent()
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 1620dc27..69c89e8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -206,6 +206,7 @@
return BouncerViewModel(
applicationContext = context,
applicationScope = applicationScope(),
+ mainDispatcher = testDispatcher,
bouncerInteractor = bouncerInteractor,
authenticationInteractor = authenticationInteractor,
flags = sceneContainerFlags,
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index 2ca84f8..30b9d0b 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -40,6 +40,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
@@ -166,9 +167,12 @@
@VisibleForTesting boolean mIsSinglePanningEnabled;
- /**
- * FullScreenMagnificationGestureHandler Constructor.
- */
+ private final FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper;
+
+ @VisibleForTesting final OverscrollHandler mOverscrollHandler;
+
+ private final boolean mIsWatch;
+
public FullScreenMagnificationGestureHandler(@UiContext Context context,
FullScreenMagnificationController fullScreenMagnificationController,
AccessibilityTraceManager trace,
@@ -254,11 +258,13 @@
mDetectingState = new DetectingState(context);
mViewportDraggingState = new ViewportDraggingState();
mPanningScalingState = new PanningScalingState(context);
- mSinglePanningState = new SinglePanningState(context,
- fullScreenMagnificationVibrationHelper);
+ mSinglePanningState = new SinglePanningState(context);
+ mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
setSinglePanningEnabled(
context.getResources()
.getBoolean(R.bool.config_enable_a11y_magnification_single_panning));
+ mOverscrollHandler = new OverscrollHandler();
+ mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
if (mDetectShortcutTrigger) {
mScreenStateReceiver = new ScreenStateReceiver(context, this);
@@ -468,15 +474,21 @@
@Override
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
int action = event.getActionMasked();
-
if (action == ACTION_POINTER_UP
&& event.getPointerCount() == 2 // includes the pointer currently being released
&& mPreviousState == mViewportDraggingState) {
-
+ // if feature flag is enabled, currently only true on watches
+ if (mIsSinglePanningEnabled) {
+ mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
+ mOverscrollHandler.clearEdgeState();
+ }
persistScaleAndTransitionTo(mViewportDraggingState);
-
} else if (action == ACTION_UP || action == ACTION_CANCEL) {
-
+ // if feature flag is enabled, currently only true on watches
+ if (mIsSinglePanningEnabled) {
+ mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
+ mOverscrollHandler.clearEdgeState();
+ }
persistScaleAndTransitionTo(mDetectingState);
}
}
@@ -502,7 +514,12 @@
}
public void persistScaleAndTransitionTo(State state) {
- mFullScreenMagnificationController.persistScale(mDisplayId);
+ // If device is a watch don't change user settings scale. On watches, warp effect
+ // is enabled and the current display scale could be differ from the default user
+ // settings scale (should not change the scale due to the warp effect)
+ if (!mIsWatch) {
+ mFullScreenMagnificationController.persistScale(mDisplayId);
+ }
clear();
transitionTo(state);
}
@@ -546,6 +563,9 @@
}
mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX,
distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
+ if (mIsSinglePanningEnabled) {
+ mOverscrollHandler.onScrollStateChanged(first, second);
+ }
return /* event consumed: */ true;
}
@@ -893,6 +913,11 @@
if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
transitionToViewportDraggingStateAndClear(event);
} else if (isActivated() && event.getPointerCount() == 2) {
+ if (mIsSinglePanningEnabled
+ && overscrollState(event, mFirstPointerDownLocation)
+ == OVERSCROLL_VERTICAL_EDGE) {
+ transitionToDelegatingStateAndClear();
+ }
//Primary pointer is swiping, so transit to PanningScalingState
transitToPanningScalingStateAndClear();
} else if (mIsSinglePanningEnabled
@@ -1264,6 +1289,7 @@
+ ", mMagnificationController=" + mFullScreenMagnificationController
+ ", mDisplayId=" + mDisplayId
+ ", mIsSinglePanningEnabled=" + mIsSinglePanningEnabled
+ + ", mOverscrollHandler=" + mOverscrollHandler
+ '}';
}
@@ -1411,32 +1437,8 @@
private final GestureDetector mScrollGestureDetector;
private MotionEventInfo mEvent;
- private final FullScreenMagnificationVibrationHelper
- mFullScreenMagnificationVibrationHelper;
-
- @VisibleForTesting int mOverscrollState;
-
- // mPivotEdge is the point on the edge of the screen when the magnified view hits the edge
- // This point sets the center of magnified view when warp/scale effect is triggered
- private final PointF mPivotEdge;
-
- // mReachedEdgeCoord is the user's pointer location on the screen when the magnified view
- // has hit the edge
- private final PointF mReachedEdgeCoord;
- // mEdgeCooldown value will be set to true when user hits the edge and will be set to false
- // once the user moves x distance away from the edge. This is so that vibrating haptic
- // doesn't get triggered by slight movements
- private boolean mEdgeCooldown;
-
- SinglePanningState(
- Context context,
- FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
+ SinglePanningState(Context context) {
mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
- mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
- mOverscrollState = OVERSCROLL_NONE;
- mPivotEdge = new PointF(Float.NaN, Float.NaN);
- mReachedEdgeCoord = new PointF(Float.NaN, Float.NaN);
- mEdgeCooldown = false;
}
@Override
@@ -1445,17 +1447,8 @@
switch (action) {
case ACTION_UP:
case ACTION_CANCEL:
- if (mOverscrollState == OVERSCROLL_LEFT_EDGE
- || mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
- mFullScreenMagnificationController.setScaleAndCenter(
- mDisplayId,
- mFullScreenMagnificationController.getPersistedScale(mDisplayId),
- mPivotEdge.x,
- mPivotEdge.y,
- true,
- AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
- }
- clear();
+ mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
+ mOverscrollHandler.clearEdgeState();
transitionTo(mDetectingState);
break;
}
@@ -1482,23 +1475,7 @@
+ " isAtEdge: "
+ mFullScreenMagnificationController.isAtEdge(mDisplayId));
}
- if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
- playEdgeVibration(second);
- setPivotEdge();
- }
- if (mOverscrollState == OVERSCROLL_NONE) {
- mOverscrollState = overscrollState(second, new PointF(first.getX(), first.getY()));
- } else if (mOverscrollState == OVERSCROLL_VERTICAL_EDGE) {
- clear();
- transitionTo(mDelegatingState);
- } else {
- boolean reset = warpEffectReset(second);
- if (reset) {
- mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
- clear();
- transitionTo(mDetectingState);
- }
- }
+ mOverscrollHandler.onScrollStateChanged(first, second);
return /* event consumed: */ true;
}
@@ -1515,35 +1492,37 @@
public String toString() {
return "SinglePanningState{"
+ "isEdgeOfView="
- + mFullScreenMagnificationController.isAtEdge(mDisplayId)
- + "overscrollStatus="
- + mOverscrollState
- + "}";
+ + mFullScreenMagnificationController.isAtEdge(mDisplayId);
}
- private void playEdgeVibration(MotionEvent event) {
- if (mOverscrollState == OVERSCROLL_NONE) {
- vibrateIfNeeded(event);
- }
+ }
+
+ /** Overscroll Handler handles the logic when user is at the edge and scrolls past an edge */
+ final class OverscrollHandler {
+
+ @VisibleForTesting int mOverscrollState;
+
+ // mPivotEdge is the point on the edge of the screen when the magnified view hits the edge
+ // This point sets the center of magnified view when warp/scale effect is triggered
+ private final PointF mPivotEdge;
+
+ // mReachedEdgeCoord is the user's pointer location on the screen when the magnified view
+ // has hit the edge
+ private final PointF mReachedEdgeCoord;
+
+ // mEdgeCooldown value will be set to true when user hits the edge and will be set to false
+ // once the user moves x distance away from the edge. This is so that vibrating haptic
+ // doesn't get triggered by slight movements
+ private boolean mEdgeCooldown;
+
+ OverscrollHandler() {
+ mOverscrollState = OVERSCROLL_NONE;
+ mPivotEdge = new PointF(Float.NaN, Float.NaN);
+ mReachedEdgeCoord = new PointF(Float.NaN, Float.NaN);
+ mEdgeCooldown = false;
}
- private void setPivotEdge() {
- if (!pointerValid(mPivotEdge)) {
- Rect bounds = new Rect();
- mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds);
- if (mOverscrollState == OVERSCROLL_LEFT_EDGE) {
- mPivotEdge.set(
- bounds.left,
- mFullScreenMagnificationController.getCenterY(mDisplayId));
- } else if (mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
- mPivotEdge.set(
- bounds.right,
- mFullScreenMagnificationController.getCenterY(mDisplayId));
- }
- }
- }
-
- private boolean warpEffectReset(MotionEvent second) {
+ protected boolean warpEffectReset(MotionEvent second) {
float scale = calculateOverscrollScale(second);
if (scale < 0) return false;
mFullScreenMagnificationController.setScaleAndCenter(
@@ -1566,7 +1545,7 @@
float overshootDistX = second.getX() - mReachedEdgeCoord.x;
if ((mOverscrollState == OVERSCROLL_LEFT_EDGE && overshootDistX < 0)
|| (mOverscrollState == OVERSCROLL_RIGHT_EDGE && overshootDistX > 0)) {
- clear();
+ clearEdgeState();
return -1.0f;
}
float overshootDistY = second.getY() - mReachedEdgeCoord.y;
@@ -1611,21 +1590,109 @@
}
private void vibrateIfNeeded(MotionEvent event) {
+ if (mOverscrollState != OVERSCROLL_NONE) {
+ return;
+ }
if ((mFullScreenMagnificationController.isAtLeftEdge(mDisplayId)
|| mFullScreenMagnificationController.isAtRightEdge(mDisplayId))
&& !mEdgeCooldown) {
mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled();
+ }
+ }
+
+ private void setPivotEdge(MotionEvent event) {
+ if (!pointerValid(mPivotEdge)) {
+ Rect bounds = new Rect();
+ mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds);
+ if (mOverscrollState == OVERSCROLL_LEFT_EDGE) {
+ mPivotEdge.set(
+ bounds.left, mFullScreenMagnificationController.getCenterY(mDisplayId));
+ } else if (mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
+ mPivotEdge.set(
+ bounds.right,
+ mFullScreenMagnificationController.getCenterY(mDisplayId));
+ }
mReachedEdgeCoord.set(event.getX(), event.getY());
mEdgeCooldown = true;
}
}
- @Override
- public void clear() {
+ private void onScrollStateChanged(MotionEvent first, MotionEvent second) {
+ if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
+ vibrateIfNeeded(second);
+ setPivotEdge(second);
+ }
+ switch (mOverscrollState) {
+ case OVERSCROLL_NONE:
+ onNoOverscroll(first, second);
+ break;
+ case OVERSCROLL_VERTICAL_EDGE:
+ onVerticalOverscroll();
+ break;
+ case OVERSCROLL_LEFT_EDGE:
+ case OVERSCROLL_RIGHT_EDGE:
+ onHorizontalOverscroll(second);
+ break;
+ default:
+ Slog.d(mLogTag, "Invalid overscroll state");
+ break;
+ }
+ }
+
+ public void onNoOverscroll(MotionEvent first, MotionEvent second) {
+ mOverscrollState = overscrollState(second, new PointF(first.getX(), first.getY()));
+ }
+
+ public void onVerticalOverscroll() {
+ clearEdgeState();
+ transitionTo(mDelegatingState);
+ }
+
+ public void onHorizontalOverscroll(MotionEvent second) {
+ boolean reset = warpEffectReset(second);
+ if (reset) {
+ mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true);
+ clearEdgeState();
+ transitionTo(mDelegatingState);
+ }
+ }
+
+ private void setScaleAndCenterToEdgeIfNeeded() {
+ if (mOverscrollState == OVERSCROLL_LEFT_EDGE
+ || mOverscrollState == OVERSCROLL_RIGHT_EDGE) {
+ mFullScreenMagnificationController.setScaleAndCenter(
+ mDisplayId,
+ mFullScreenMagnificationController.getPersistedScale(mDisplayId),
+ mPivotEdge.x,
+ mPivotEdge.y,
+ true,
+ AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
+ }
+ }
+
+ private void clearEdgeState() {
mOverscrollState = OVERSCROLL_NONE;
mPivotEdge.set(Float.NaN, Float.NaN);
mReachedEdgeCoord.set(Float.NaN, Float.NaN);
mEdgeCooldown = false;
}
+
+ @Override
+ public String toString() {
+ return "OverscrollHandler {"
+ + "mOverscrollState="
+ + mOverscrollState
+ + "mPivotEdge.x="
+ + mPivotEdge.x
+ + "mPivotEdge.y="
+ + mPivotEdge.y
+ + "mReachedEdgeCoord.x="
+ + mReachedEdgeCoord.x
+ + "mReachedEdgeCoord.y="
+ + mReachedEdgeCoord.y
+ + "mEdgeCooldown="
+ + mEdgeCooldown
+ + "}";
+ }
}
}
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionDefinitionsTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionDefinitionsTest.kt
new file mode 100644
index 0000000..832136c
--- /dev/null
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionDefinitionsTest.kt
@@ -0,0 +1,764 @@
+/*
+ * 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.server.permission.test
+
+import android.content.pm.PermissionInfo
+import android.os.Build
+import com.android.server.permission.access.MutateStateScope
+import com.android.server.permission.access.permission.Permission
+import com.android.server.permission.access.permission.PermissionFlags
+import com.android.server.pm.pkg.PackageState
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Parameterized test for testing permission definitions (adopt permissions, add permission groups,
+ * add permissions, trim permissions, trim permission states and revoke permissions on
+ * package update) for onStorageVolumeAdded() and onPackageAdded() in AppIdPermissionPolicy.
+ *
+ * Note that the evaluatePermissionState() call with permission changes
+ * (i.e. changedPermissionNames in AppIdPermissionPolicy) and the evaluatePermissionState() call
+ * with an installedPackageState is put in this test instead of
+ * AppIdPermissionPolicyPermissionStatesTest because these concepts don't apply to onUserAdded().
+ */
+@RunWith(Parameterized::class)
+class AppIdPermissionPolicyPermissionDefinitionsTest : BaseAppIdPermissionPolicyTest() {
+ @Parameterized.Parameter(0) lateinit var action: Action
+
+ @Test
+ fun testAdoptPermissions_permissionsOfMissingSystemApp_getsAdopted() {
+ testAdoptPermissions(hasMissingPackage = true, isSystem = true)
+
+ assertWithMessage(
+ "After $action is called for a null adopt permission package," +
+ " the permission package name: ${getPermission(PERMISSION_NAME_0)?.packageName}" +
+ " did not match the expected package name: $PACKAGE_NAME_0"
+ )
+ .that(getPermission(PERMISSION_NAME_0)?.packageName)
+ .isEqualTo(PACKAGE_NAME_0)
+ }
+
+ @Test
+ fun testAdoptPermissions_permissionsOfExistingSystemApp_notAdopted() {
+ testAdoptPermissions(isSystem = true)
+
+ assertWithMessage(
+ "After $action is called for a non-null adopt permission" +
+ " package, the permission package name:" +
+ " ${getPermission(PERMISSION_NAME_0)?.packageName} should not match the" +
+ " package name: $PACKAGE_NAME_0"
+ )
+ .that(getPermission(PERMISSION_NAME_0)?.packageName)
+ .isNotEqualTo(PACKAGE_NAME_0)
+ }
+
+ @Test
+ fun testAdoptPermissions_permissionsOfNonSystemApp_notAdopted() {
+ testAdoptPermissions(hasMissingPackage = true)
+
+ assertWithMessage(
+ "After $action is called for a non-system adopt permission" +
+ " package, the permission package name:" +
+ " ${getPermission(PERMISSION_NAME_0)?.packageName} should not match the" +
+ " package name: $PACKAGE_NAME_0"
+ )
+ .that(getPermission(PERMISSION_NAME_0)?.packageName)
+ .isNotEqualTo(PACKAGE_NAME_0)
+ }
+
+ private fun testAdoptPermissions(
+ hasMissingPackage: Boolean = false,
+ isSystem: Boolean = false
+ ) {
+ val parsedPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_1)
+ val packageToAdoptPermission = if (hasMissingPackage) {
+ mockPackageState(APP_ID_1, PACKAGE_NAME_1, isSystem = isSystem)
+ } else {
+ mockPackageState(
+ APP_ID_1,
+ mockAndroidPackage(
+ PACKAGE_NAME_1,
+ permissions = listOf(parsedPermission)
+ ),
+ isSystem = isSystem
+ )
+ }
+ addPackageState(packageToAdoptPermission)
+ addPermission(parsedPermission)
+
+ mutateState {
+ val installedPackage = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(
+ PACKAGE_NAME_0,
+ permissions = listOf(defaultPermission),
+ adoptPermissions = listOf(PACKAGE_NAME_1)
+ )
+ )
+ addPackageState(installedPackage, newState)
+ testAction(installedPackage)
+ }
+ }
+
+ @Test
+ fun testPermissionGroupDefinition_newPermissionGroup_getsDeclared() {
+ mutateState {
+ val packageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+ addPackageState(packageState, newState)
+ testAction(packageState)
+ }
+
+ assertWithMessage(
+ "After $action is called when there is no existing" +
+ " permission groups, the new permission group $PERMISSION_GROUP_NAME_0 is not added"
+ )
+ .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.name)
+ .isEqualTo(PERMISSION_GROUP_NAME_0)
+ }
+
+ @Test
+ fun testPermissionGroupDefinition_systemAppTakingOverPermissionGroupDefinition_getsTakenOver() {
+ testTakingOverPermissionAndPermissionGroupDefinitions(newPermissionOwnerIsSystem = true)
+
+ assertWithMessage(
+ "After $action is called when $PERMISSION_GROUP_NAME_0 already" +
+ " exists in the system, the system app $PACKAGE_NAME_0 didn't takeover the" +
+ " ownership of this permission group"
+ )
+ .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
+ .isEqualTo(PACKAGE_NAME_0)
+ }
+
+ @Test
+ fun testPermissionGroupDefinition_instantApps_remainsUnchanged() {
+ testTakingOverPermissionAndPermissionGroupDefinitions(
+ newPermissionOwnerIsInstant = true,
+ permissionGroupAlreadyExists = false
+ )
+
+ assertWithMessage(
+ "After $action is called for an instant app," +
+ " the new permission group $PERMISSION_GROUP_NAME_0 should not be added"
+ )
+ .that(getPermissionGroup(PERMISSION_GROUP_NAME_0))
+ .isNull()
+ }
+
+ @Test
+ fun testPermissionGroupDefinition_nonSystemAppTakingOverGroupDefinition_remainsUnchanged() {
+ testTakingOverPermissionAndPermissionGroupDefinitions()
+
+ assertWithMessage(
+ "After $action is called when $PERMISSION_GROUP_NAME_0 already" +
+ " exists in the system, non-system app $PACKAGE_NAME_0 shouldn't takeover" +
+ " ownership of this permission group"
+ )
+ .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
+ .isEqualTo(PACKAGE_NAME_1)
+ }
+
+ @Test
+ fun testPermissionGroupDefinition_takingOverGroupDeclaredBySystemApp_remainsUnchanged() {
+ testTakingOverPermissionAndPermissionGroupDefinitions(oldPermissionOwnerIsSystem = true)
+
+ assertWithMessage(
+ "After $action is called when $PERMISSION_GROUP_NAME_0 already" +
+ " exists in the system and is owned by a system app, app $PACKAGE_NAME_0" +
+ " shouldn't takeover ownership of this permission group"
+ )
+ .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
+ .isEqualTo(PACKAGE_NAME_1)
+ }
+
+ @Test
+ fun testPermissionDefinition_newPermission_getsDeclared() {
+ mutateState {
+ val packageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+ addPackageState(packageState, newState)
+ testAction(packageState)
+ }
+
+ assertWithMessage(
+ "After $action is called when there is no existing" +
+ " permissions, the new permission $PERMISSION_NAME_0 is not added"
+ )
+ .that(getPermission(PERMISSION_NAME_0)?.name)
+ .isEqualTo(PERMISSION_NAME_0)
+ }
+
+ @Test
+ fun testPermissionDefinition_configPermission_getsTakenOver() {
+ testTakingOverPermissionAndPermissionGroupDefinitions(
+ oldPermissionOwnerIsSystem = true,
+ newPermissionOwnerIsSystem = true,
+ type = Permission.TYPE_CONFIG,
+ isReconciled = false
+ )
+
+ assertWithMessage(
+ "After $action is called for a config permission with" +
+ " no owner, the ownership is not taken over by a system app $PACKAGE_NAME_0"
+ )
+ .that(getPermission(PERMISSION_NAME_0)?.packageName)
+ .isEqualTo(PACKAGE_NAME_0)
+ }
+
+ @Test
+ fun testPermissionDefinition_systemAppTakingOverPermissionDefinition_getsTakenOver() {
+ testTakingOverPermissionAndPermissionGroupDefinitions(newPermissionOwnerIsSystem = true)
+
+ assertWithMessage(
+ "After $action is called when $PERMISSION_NAME_0 already" +
+ " exists in the system, the system app $PACKAGE_NAME_0 didn't takeover ownership" +
+ " of this permission"
+ )
+ .that(getPermission(PERMISSION_NAME_0)?.packageName)
+ .isEqualTo(PACKAGE_NAME_0)
+ }
+
+ @Test
+ fun testPermissionDefinition_nonSystemAppTakingOverPermissionDefinition_remainsUnchanged() {
+ testTakingOverPermissionAndPermissionGroupDefinitions()
+
+ assertWithMessage(
+ "After $action is called when $PERMISSION_NAME_0 already" +
+ " exists in the system, the non-system app $PACKAGE_NAME_0 shouldn't takeover" +
+ " ownership of this permission"
+ )
+ .that(getPermission(PERMISSION_NAME_0)?.packageName)
+ .isEqualTo(PACKAGE_NAME_1)
+ }
+
+ @Test
+ fun testPermissionDefinition_takingOverPermissionDeclaredBySystemApp_remainsUnchanged() {
+ testTakingOverPermissionAndPermissionGroupDefinitions(oldPermissionOwnerIsSystem = true)
+
+ assertWithMessage(
+ "After $action is called when $PERMISSION_NAME_0 already" +
+ " exists in system and is owned by a system app, the $PACKAGE_NAME_0 shouldn't" +
+ " takeover ownership of this permission"
+ )
+ .that(getPermission(PERMISSION_NAME_0)?.packageName)
+ .isEqualTo(PACKAGE_NAME_1)
+ }
+
+ private fun testTakingOverPermissionAndPermissionGroupDefinitions(
+ oldPermissionOwnerIsSystem: Boolean = false,
+ newPermissionOwnerIsSystem: Boolean = false,
+ newPermissionOwnerIsInstant: Boolean = false,
+ permissionGroupAlreadyExists: Boolean = true,
+ permissionAlreadyExists: Boolean = true,
+ type: Int = Permission.TYPE_MANIFEST,
+ isReconciled: Boolean = true,
+ ) {
+ val oldPermissionOwnerPackageState = mockPackageState(
+ APP_ID_1,
+ PACKAGE_NAME_1,
+ isSystem = oldPermissionOwnerIsSystem
+ )
+ addPackageState(oldPermissionOwnerPackageState)
+ if (permissionGroupAlreadyExists) {
+ addPermissionGroup(mockParsedPermissionGroup(PERMISSION_GROUP_NAME_0, PACKAGE_NAME_1))
+ }
+ if (permissionAlreadyExists) {
+ addPermission(
+ mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_1),
+ type = type,
+ isReconciled = isReconciled
+ )
+ }
+
+ mutateState {
+ val newPermissionOwnerPackageState = mockPackageState(
+ APP_ID_0,
+ mockSimpleAndroidPackage(),
+ isSystem = newPermissionOwnerIsSystem,
+ isInstantApp = newPermissionOwnerIsInstant
+ )
+ addPackageState(newPermissionOwnerPackageState, newState)
+ testAction(newPermissionOwnerPackageState)
+ }
+ }
+
+ @Test
+ fun testPermissionChanged_permissionGroupChanged_getsRevoked() {
+ testPermissionChanged(
+ oldPermissionGroup = PERMISSION_GROUP_NAME_1,
+ newPermissionGroup = PERMISSION_GROUP_NAME_0
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that has a permission group change" +
+ " for a permission it defines, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testPermissionChanged_protectionLevelChanged_getsRevoked() {
+ testPermissionChanged(newProtectionLevel = PermissionInfo.PROTECTION_INTERNAL)
+
+ val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that has a protection level change" +
+ " for a permission it defines, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ private fun testPermissionChanged(
+ oldPermissionGroup: String? = null,
+ newPermissionGroup: String? = null,
+ newProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS
+ ) {
+ val oldPermission = mockParsedPermission(
+ PERMISSION_NAME_0,
+ PACKAGE_NAME_0,
+ group = oldPermissionGroup,
+ protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
+ )
+ val oldPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(oldPermission))
+ )
+ addPackageState(oldPackageState)
+ addPermission(oldPermission)
+ setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.RUNTIME_GRANTED)
+
+ mutateState {
+ val newPermission = mockParsedPermission(
+ PERMISSION_NAME_0,
+ PACKAGE_NAME_0,
+ group = newPermissionGroup,
+ protectionLevel = newProtectionLevel
+ )
+ val newPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(newPermission))
+ )
+ addPackageState(newPackageState, newState)
+ testAction(newPackageState)
+ }
+ }
+
+ @Test
+ fun testPermissionDeclaration_permissionTreeNoLongerDeclared_getsDefinitionRemoved() {
+ testPermissionDeclaration {}
+
+ assertWithMessage(
+ "After $action is called for a package that no longer defines a permission" +
+ " tree, the permission tree: $PERMISSION_NAME_0 in system state should be removed"
+ )
+ .that(getPermissionTree(PERMISSION_NAME_0))
+ .isNull()
+ }
+
+ @Test
+ fun testPermissionDeclaration_permissionTreeByDisabledSystemPackage_remainsUnchanged() {
+ testPermissionDeclaration {
+ val disabledSystemPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+ addDisabledSystemPackageState(disabledSystemPackageState)
+ }
+
+ assertWithMessage(
+ "After $action is called for a package that no longer defines" +
+ " a permission tree while this permission tree is still defined by" +
+ " a disabled system package, the permission tree: $PERMISSION_NAME_0 in" +
+ " system state should not be removed"
+ )
+ .that(getPermissionTree(PERMISSION_TREE_NAME))
+ .isNotNull()
+ }
+
+ @Test
+ fun testPermissionDeclaration_permissionNoLongerDeclared_getsDefinitionRemoved() {
+ testPermissionDeclaration {}
+
+ assertWithMessage(
+ "After $action is called for a package that no longer defines a permission," +
+ " the permission: $PERMISSION_NAME_0 in system state should be removed"
+ )
+ .that(getPermission(PERMISSION_NAME_0))
+ .isNull()
+ }
+
+ @Test
+ fun testPermissionDeclaration_permissionByDisabledSystemPackage_remainsUnchanged() {
+ testPermissionDeclaration {
+ val disabledSystemPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+ addDisabledSystemPackageState(disabledSystemPackageState)
+ }
+
+ assertWithMessage(
+ "After $action is called for a disabled system package and it's updated apk" +
+ " no longer defines a permission, the permission: $PERMISSION_NAME_0 in" +
+ " system state should not be removed"
+ )
+ .that(getPermission(PERMISSION_NAME_0))
+ .isNotNull()
+ }
+
+ private fun testPermissionDeclaration(additionalSetup: () -> Unit) {
+ val oldPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+ addPackageState(oldPackageState)
+ addPermission(defaultPermissionTree)
+ addPermission(defaultPermission)
+
+ additionalSetup()
+
+ mutateState {
+ val newPackageState = mockPackageState(APP_ID_0, mockAndroidPackage(PACKAGE_NAME_0))
+ addPackageState(newPackageState, newState)
+ testAction(newPackageState)
+ }
+ }
+
+ @Test
+ fun testTrimPermissionStates_permissionsNoLongerRequested_getsFlagsRevoked() {
+ val parsedPermission = mockParsedPermission(
+ PERMISSION_NAME_0,
+ PACKAGE_NAME_0,
+ protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
+ )
+ val oldPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(
+ PACKAGE_NAME_0,
+ permissions = listOf(parsedPermission),
+ requestedPermissions = setOf(PERMISSION_NAME_0)
+ )
+ )
+ addPackageState(oldPackageState)
+ addPermission(parsedPermission)
+ setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.RUNTIME_GRANTED)
+
+ mutateState {
+ val newPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+ addPackageState(newPackageState, newState)
+ testAction(newPackageState)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that no longer requests a permission" +
+ " the actual permission flags $actualFlags should match the" +
+ " expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testRevokePermissionsOnPackageUpdate_storageAndMediaDowngradingPastQ_getsRuntimeRevoked() {
+ testRevokePermissionsOnPackageUpdate(
+ PermissionFlags.RUNTIME_GRANTED,
+ newTargetSdkVersion = Build.VERSION_CODES.P
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that's downgrading past Q" +
+ " the actual permission flags $actualFlags should match the" +
+ " expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testRevokePermissionsOnPackageUpdate_storageAndMediaNotDowngradingPastQ_remainsUnchanged() {
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testRevokePermissionsOnPackageUpdate(
+ oldFlags,
+ oldTargetSdkVersion = Build.VERSION_CODES.P,
+ newTargetSdkVersion = Build.VERSION_CODES.P
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that's not downgrading past Q" +
+ " the actual permission flags $actualFlags should match the" +
+ " expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testRevokePermissionsOnPackageUpdate_policyFixedDowngradingPastQ_remainsUnchanged() {
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED and PermissionFlags.POLICY_FIXED
+ testRevokePermissionsOnPackageUpdate(oldFlags, newTargetSdkVersion = Build.VERSION_CODES.P)
+
+ val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that's downgrading past Q" +
+ " the actual permission flags with PermissionFlags.POLICY_FIXED $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testRevokePermissionsOnPackageUpdate_newlyRequestingLegacyExternalStorage_runtimeRevoked() {
+ testRevokePermissionsOnPackageUpdate(
+ PermissionFlags.RUNTIME_GRANTED,
+ oldTargetSdkVersion = Build.VERSION_CODES.P,
+ newTargetSdkVersion = Build.VERSION_CODES.P,
+ oldIsRequestLegacyExternalStorage = false
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package with" +
+ " newlyRequestingLegacyExternalStorage, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testRevokePermissionsOnPackageUpdate_missingOldPackage_remainsUnchanged() {
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testRevokePermissionsOnPackageUpdate(
+ oldFlags,
+ newTargetSdkVersion = Build.VERSION_CODES.P,
+ isOldPackageMissing = true
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that's downgrading past Q" +
+ " and doesn't have the oldPackage, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ private fun testRevokePermissionsOnPackageUpdate(
+ oldFlags: Int,
+ oldTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+ newTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+ oldIsRequestLegacyExternalStorage: Boolean = true,
+ newIsRequestLegacyExternalStorage: Boolean = true,
+ isOldPackageMissing: Boolean = false
+ ) {
+ val parsedPermission = mockParsedPermission(
+ PERMISSION_READ_EXTERNAL_STORAGE,
+ PACKAGE_NAME_0,
+ protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
+ )
+ val oldPackageState = if (isOldPackageMissing) {
+ mockPackageState(APP_ID_0, PACKAGE_NAME_0)
+ } else {
+ mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(
+ PACKAGE_NAME_0,
+ targetSdkVersion = oldTargetSdkVersion,
+ isRequestLegacyExternalStorage = oldIsRequestLegacyExternalStorage,
+ requestedPermissions = setOf(PERMISSION_READ_EXTERNAL_STORAGE),
+ permissions = listOf(parsedPermission)
+ )
+ )
+ }
+ addPackageState(oldPackageState)
+ addPermission(parsedPermission)
+ setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE, oldFlags)
+
+ mutateState {
+ val newPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(
+ PACKAGE_NAME_0,
+ targetSdkVersion = newTargetSdkVersion,
+ isRequestLegacyExternalStorage = newIsRequestLegacyExternalStorage,
+ requestedPermissions = setOf(PERMISSION_READ_EXTERNAL_STORAGE),
+ permissions = listOf(parsedPermission)
+ )
+ )
+ addPackageState(newPackageState, newState)
+ testAction(newPackageState)
+ }
+ }
+
+ @Test
+ fun testEvaluatePermissionState_normalPermissionRequestedByInstalledPackage_getsGranted() {
+ val oldFlags = PermissionFlags.INSTALL_REVOKED
+ val permissionOwnerPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
+ val installedPackageState = mockPackageState(
+ APP_ID_1,
+ mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0))
+ )
+ addPackageState(permissionOwnerPackageState)
+ addPackageState(installedPackageState)
+ addPermission(defaultPermission)
+ setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags)
+
+ mutateState {
+ testAction(installedPackageState)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a normal permission" +
+ " with the INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags since it's a new install"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * We set up a permission protection level change from SIGNATURE to NORMAL in order to make
+ * the permission a "changed permission" in order to test evaluatePermissionState() called by
+ * evaluatePermissionStateForAllPackages(). This makes the requestingPackageState not the
+ * installedPackageState so that we can test whether requesting by system package will give us
+ * the expected permission flags.
+ *
+ * Besides, this also helps us test evaluatePermissionStateForAllPackages(). Since both
+ * evaluatePermissionStateForAllPackages() and evaluateAllPermissionStatesForPackage() call
+ * evaluatePermissionState() in their implementations, we use these tests as the only tests
+ * that test evaluatePermissionStateForAllPackages()
+ */
+ @Test
+ fun testEvaluatePermissionState_normalPermissionRequestedBySystemPackage_getsGranted() {
+ testEvaluateNormalPermissionStateWithPermissionChanges(requestingPackageIsSystem = true)
+
+ val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+ assertWithMessage(
+ "After $action is called for a system package that requests a normal" +
+ " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_normalCompatibilityPermission_getsGranted() {
+ testEvaluateNormalPermissionStateWithPermissionChanges(
+ permissionName = PERMISSION_POST_NOTIFICATIONS,
+ requestingPackageTargetSdkVersion = Build.VERSION_CODES.S
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_POST_NOTIFICATIONS)
+ val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a normal compatibility" +
+ " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_normalPermissionPreviouslyRevoked_getsInstallRevoked() {
+ testEvaluateNormalPermissionStateWithPermissionChanges()
+
+ val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.INSTALL_REVOKED
+ assertWithMessage(
+ "After $action is called for a package that requests a normal" +
+ " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ private fun testEvaluateNormalPermissionStateWithPermissionChanges(
+ permissionName: String = PERMISSION_NAME_0,
+ requestingPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+ requestingPackageIsSystem: Boolean = false
+ ) {
+ val oldParsedPermission = mockParsedPermission(
+ permissionName,
+ PACKAGE_NAME_0,
+ protectionLevel = PermissionInfo.PROTECTION_SIGNATURE
+ )
+ val oldPermissionOwnerPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(oldParsedPermission))
+ )
+ val requestingPackageState = mockPackageState(
+ APP_ID_1,
+ mockAndroidPackage(
+ PACKAGE_NAME_1,
+ requestedPermissions = setOf(permissionName),
+ targetSdkVersion = requestingPackageTargetSdkVersion
+ ),
+ isSystem = requestingPackageIsSystem,
+ )
+ addPackageState(oldPermissionOwnerPackageState)
+ addPackageState(requestingPackageState)
+ addPermission(oldParsedPermission)
+ val oldFlags = PermissionFlags.INSTALL_REVOKED
+ setPermissionFlags(APP_ID_1, USER_ID_0, permissionName, oldFlags)
+
+ mutateState {
+ val newParsedPermission = mockParsedPermission(permissionName, PACKAGE_NAME_0)
+ val newPermissionOwnerPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(newParsedPermission))
+ )
+ addPackageState(newPermissionOwnerPackageState, newState)
+ testAction(newPermissionOwnerPackageState)
+ }
+ }
+
+ private fun MutateStateScope.testAction(packageState: PackageState) {
+ with(appIdPermissionPolicy) {
+ when (action) {
+ Action.ON_PACKAGE_ADDED -> onPackageAdded(packageState)
+ Action.ON_STORAGE_VOLUME_ADDED -> onStorageVolumeMounted(
+ null,
+ listOf(packageState.packageName),
+ true
+ )
+ }
+ }
+ }
+
+ enum class Action { ON_PACKAGE_ADDED, ON_STORAGE_VOLUME_ADDED }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun data(): Array<Action> = Action.values()
+ }
+}
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt
new file mode 100644
index 0000000..823ce45
--- /dev/null
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.server.permission.test
+
+import android.content.pm.PermissionInfo
+import com.android.server.permission.access.MutateStateScope
+import com.android.server.permission.access.permission.PermissionFlags
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * A parameterized test for testing resetting runtime permissions for onPackageUninstalled()
+ * and resetRuntimePermissions() in AppIdPermissionPolicy
+ */
+@RunWith(Parameterized::class)
+class AppIdPermissionPolicyPermissionResetTest : BaseAppIdPermissionPolicyTest() {
+ @Parameterized.Parameter(0) lateinit var action: Action
+
+ @Test
+ fun testResetRuntimePermissions_runtimeGranted_getsRevoked() {
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ val expectedNewFlags = 0
+ testResetRuntimePermissions(oldFlags, expectedNewFlags)
+ }
+
+ @Test
+ fun testResetRuntimePermissions_roleGranted_getsGranted() {
+ val oldFlags = PermissionFlags.ROLE
+ val expectedNewFlags = PermissionFlags.ROLE or PermissionFlags.RUNTIME_GRANTED
+ testResetRuntimePermissions(oldFlags, expectedNewFlags)
+ }
+
+ @Test
+ fun testResetRuntimePermissions_nullAndroidPackage_remainsUnchanged() {
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
+ testResetRuntimePermissions(oldFlags, expectedNewFlags, isAndroidPackageMissing = true)
+ }
+
+ private fun testResetRuntimePermissions(
+ oldFlags: Int,
+ expectedNewFlags: Int,
+ isAndroidPackageMissing: Boolean = false
+ ) {
+ val parsedPermission = mockParsedPermission(
+ PERMISSION_NAME_0,
+ PACKAGE_NAME_0,
+ protectionLevel = PermissionInfo.PROTECTION_DANGEROUS,
+ )
+ val permissionOwnerPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
+ )
+ val requestingPackageState = if (isAndroidPackageMissing) {
+ mockPackageState(APP_ID_1, PACKAGE_NAME_1)
+ } else {
+ mockPackageState(
+ APP_ID_1,
+ mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0))
+ )
+ }
+ setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags)
+ addPackageState(permissionOwnerPackageState)
+ addPackageState(requestingPackageState)
+ addPermission(parsedPermission)
+
+ mutateState { testAction() }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+ assertWithMessage(
+ "After resetting runtime permissions, permission flags did not match" +
+ " expected values: expectedNewFlags is $expectedNewFlags," +
+ " actualFlags is $actualFlags, while the oldFlags is $oldFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ private fun MutateStateScope.testAction(
+ packageName: String = PACKAGE_NAME_1,
+ appId: Int = APP_ID_1,
+ userId: Int = USER_ID_0
+ ) {
+ with(appIdPermissionPolicy) {
+ when (action) {
+ Action.ON_PACKAGE_UNINSTALLED -> onPackageUninstalled(packageName, appId, userId)
+ Action.RESET_RUNTIME_PERMISSIONS -> resetRuntimePermissions(packageName, userId)
+ }
+ }
+ }
+
+ enum class Action { ON_PACKAGE_UNINSTALLED, RESET_RUNTIME_PERMISSIONS }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun data(): Array<Action> = Action.values()
+ }
+}
\ No newline at end of file
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
new file mode 100644
index 0000000..f085bd7
--- /dev/null
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
@@ -0,0 +1,884 @@
+/*
+ * 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.server.permission.test
+
+import android.content.pm.PermissionInfo
+import android.os.Build
+import com.android.server.permission.access.MutableAccessState
+import com.android.server.permission.access.MutateStateScope
+import com.android.server.permission.access.immutable.IndexedListSet
+import com.android.server.permission.access.immutable.MutableIndexedListSet
+import com.android.server.permission.access.immutable.MutableIndexedMap
+import com.android.server.permission.access.permission.PermissionFlags
+import com.android.server.pm.permission.PermissionAllowlist
+import com.android.server.pm.pkg.PackageState
+import com.android.server.testutils.mock
+import com.android.server.testutils.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * A parameterized test for testing evaluating permission states and inheriting implicit permission
+ * states for onUserAdded(), onStorageVolumeAdded() and onPackageAdded() in AppIdPermissionPolicy
+ */
+@RunWith(Parameterized::class)
+class AppIdPermissionPolicyPermissionStatesTest : BaseAppIdPermissionPolicyTest() {
+ @Parameterized.Parameter(0) lateinit var action: Action
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ if (action == Action.ON_USER_ADDED) {
+ createUserState(USER_ID_NEW)
+ }
+ }
+
+ @Test
+ fun testEvaluatePermissionState_normalPermissionAlreadyGranted_remainsUnchanged() {
+ val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.INSTALL_REVOKED
+ testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_NORMAL) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests a normal permission" +
+ " with an existing INSTALL_GRANTED flag, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_normalPermissionNotInstallRevoked_getsGranted() {
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_NORMAL,
+ isNewInstall = true
+ ) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a normal permission" +
+ " with no existing flags, the actual permission flags $actualFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_normalAppOpPermission_getsRoleAndUserSetFlagsPreserved() {
+ val oldFlags = PermissionFlags.ROLE or PermissionFlags.USER_SET
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_NORMAL or PermissionInfo.PROTECTION_FLAG_APPOP
+ ) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.INSTALL_GRANTED or oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests a normal app op" +
+ " permission with existing ROLE and USER_SET flags, the actual permission flags" +
+ " $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_internalWasGrantedWithMissingPackage_getsProtectionGranted() {
+ val oldFlags = PermissionFlags.PROTECTION_GRANTED
+ testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_INTERNAL) {
+ val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
+ addPackageState(packageStateWithMissingPackage)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests an internal permission" +
+ " with missing android package and $oldFlags flag, the actual permission flags" +
+ " $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_internalAppOpPermission_getsRoleAndUserSetFlagsPreserved() {
+ val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or
+ PermissionFlags.USER_SET
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_APPOP
+ ) {
+ val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
+ addPackageState(packageStateWithMissingPackage)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests an internal permission" +
+ " with missing android package and $oldFlags flag and the permission isAppOp," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_internalDevelopmentPermission_getsRuntimeGrantedPreserved() {
+ val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_DEVELOPMENT
+ ) {
+ val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
+ addPackageState(packageStateWithMissingPackage)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests an internal permission" +
+ " with missing android package and $oldFlags flag and permission isDevelopment," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_internalRolePermission_getsRoleAndRuntimeGrantedPreserved() {
+ val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or
+ PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_ROLE
+ ) {
+ val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
+ addPackageState(packageStateWithMissingPackage)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests an internal permission" +
+ " with missing android package and $oldFlags flag and the permission isRole," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_signaturePrivilegedPermissionNotAllowlisted_isNotGranted() {
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_SIGNATURE or PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
+ isInstalledPackageSystem = true,
+ isInstalledPackagePrivileged = true,
+ isInstalledPackageProduct = true,
+ // To mock the return value of shouldGrantPrivilegedOrOemPermission()
+ isInstalledPackageVendor = true,
+ isNewInstall = true
+ ) {
+ val platformPackage = mockPackageState(
+ PLATFORM_APP_ID,
+ mockAndroidPackage(PLATFORM_PACKAGE_NAME)
+ )
+ setupAllowlist(PACKAGE_NAME_1, false)
+ addPackageState(platformPackage)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests a signature privileged" +
+ " permission that's not allowlisted, the actual permission" +
+ " flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_nonPrivilegedShouldGrantBySignature_getsProtectionGranted() {
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_SIGNATURE,
+ isInstalledPackageSystem = true,
+ isInstalledPackagePrivileged = true,
+ isInstalledPackageProduct = true,
+ isInstalledPackageSignatureMatching = true,
+ isInstalledPackageVendor = true,
+ isNewInstall = true
+ ) {
+ val platformPackage = mockPackageState(
+ PLATFORM_APP_ID,
+ mockAndroidPackage(PLATFORM_PACKAGE_NAME, isSignatureMatching = true)
+ )
+ setupAllowlist(PACKAGE_NAME_1, false)
+ addPackageState(platformPackage)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a signature" +
+ " non-privileged permission, the actual permission" +
+ " flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_privilegedAllowlistShouldGrantByProtectionFlags_getsGranted() {
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_SIGNATURE or PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
+ isInstalledPackageSystem = true,
+ isInstalledPackagePrivileged = true,
+ isInstalledPackageProduct = true,
+ isNewInstall = true
+ ) {
+ val platformPackage = mockPackageState(
+ PLATFORM_APP_ID,
+ mockAndroidPackage(PLATFORM_PACKAGE_NAME)
+ )
+ setupAllowlist(PACKAGE_NAME_1, true)
+ addPackageState(platformPackage)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a signature privileged" +
+ " permission that's allowlisted and should grant by protection flags, the actual" +
+ " permission flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ private fun setupAllowlist(
+ packageName: String,
+ allowlistState: Boolean,
+ state: MutableAccessState = oldState
+ ) {
+ state.mutateExternalState().setPrivilegedPermissionAllowlistPackages(
+ MutableIndexedListSet<String>().apply { add(packageName) }
+ )
+ val mockAllowlist = mock<PermissionAllowlist> {
+ whenever(
+ getProductPrivilegedAppAllowlistState(packageName, PERMISSION_NAME_0)
+ ).thenReturn(allowlistState)
+ }
+ state.mutateExternalState().setPermissionAllowlist(mockAllowlist)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_nonRuntimeFlagsOnRuntimePermissions_getsCleared() {
+ val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.PREGRANT or
+ PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.PREGRANT or PermissionFlags.RUNTIME_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime permission" +
+ " with existing $oldFlags flags, the actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_newPermissionsForPreM_requiresUserReview() {
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP,
+ isNewInstall = true
+ ) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime permission" +
+ " with no existing flags in pre M, actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_legacyOrImplicitGrantedPreviouslyRevoked_getsAppOpRevoked() {
+ val oldFlags = PermissionFlags.USER_FIXED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP
+ ) {
+ setPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0, oldFlags)
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.USER_FIXED or
+ PermissionFlags.APP_OP_REVOKED
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime permission" +
+ " that should be LEGACY_GRANTED or IMPLICIT_GRANTED that was previously revoked," +
+ " the actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_legacyGrantedForPostM_userReviewRequirementRemoved() {
+ val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT
+ testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime permission" +
+ " that used to require user review, the user review requirement should be removed" +
+ " if it's upgraded to post M. The actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_legacyGrantedPermissionsAlreadyReviewedForPostM_getsGranted() {
+ val oldFlags = PermissionFlags.LEGACY_GRANTED
+ testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime permission" +
+ " that was already reviewed by the user, the permission should be RUNTIME_GRANTED" +
+ " if it's upgraded to post M. The actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_leanbackNotificationPermissionsForPostM_getsImplicitGranted() {
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_POST_NOTIFICATIONS,
+ isNewInstall = true
+ ) {
+ oldState.mutateExternalState().setLeanback(true)
+ }
+
+ val actualFlags = getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_POST_NOTIFICATIONS
+ )
+ val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime notification" +
+ " permission when isLeanback, the actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_implicitSourceFromNonRuntime_getsImplicitGranted() {
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ implicitPermissions = setOf(PERMISSION_NAME_0),
+ isNewInstall = true
+ ) {
+ oldState.mutateExternalState().setImplicitToSourcePermissions(
+ MutableIndexedMap<String, IndexedListSet<String>>().apply {
+ put(PERMISSION_NAME_0, MutableIndexedListSet<String>().apply {
+ add(PERMISSION_NAME_1)
+ })
+ }
+ )
+ addPermission(mockParsedPermission(PERMISSION_NAME_1, PACKAGE_NAME_0))
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime implicit" +
+ " permission that's source from a non-runtime permission, the actual permission" +
+ " flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * For a legacy granted or implicit permission during the app upgrade, when the permission
+ * should no longer be legacy or implicit granted, we want to remove the APP_OP_REVOKED flag
+ * so that the app can request the permission.
+ */
+ @Test
+ fun testEvaluatePermissionState_noLongerLegacyOrImplicitGranted_canBeRequested() {
+ val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.APP_OP_REVOKED or
+ PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime permission" +
+ " that is no longer LEGACY_GRANTED or IMPLICIT_GRANTED, the actual permission" +
+ " flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_noLongerImplicit_getsRuntimeAndImplicitFlagsRemoved() {
+ val oldFlags = PermissionFlags.IMPLICIT or PermissionFlags.RUNTIME_GRANTED or
+ PermissionFlags.USER_SET or PermissionFlags.USER_FIXED
+ testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime permission" +
+ " that is no longer implicit and we shouldn't retain as nearby device" +
+ " permissions, the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_noLongerImplicitNearbyWasGranted_getsRuntimeGranted() {
+ val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BLUETOOTH_CONNECT,
+ requestedPermissions = setOf(
+ PERMISSION_BLUETOOTH_CONNECT,
+ PERMISSION_ACCESS_BACKGROUND_LOCATION
+ )
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_ACCESS_BACKGROUND_LOCATION,
+ PermissionFlags.RUNTIME_GRANTED
+ )
+ }
+
+ val actualFlags = getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BLUETOOTH_CONNECT
+ )
+ val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime nearby device" +
+ " permission that was granted by implicit, the actual permission flags" +
+ " $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_noLongerImplicitSystemOrPolicyFixedWasGranted_runtimeGranted() {
+ val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT or
+ PermissionFlags.SYSTEM_FIXED
+ testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.SYSTEM_FIXED
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime permission" +
+ " that was granted and is no longer implicit and is SYSTEM_FIXED or POLICY_FIXED," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_restrictedPermissionsNotExempt_getsRestrictionFlags() {
+ val oldFlags = PermissionFlags.RESTRICTION_REVOKED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionInfoFlags = PermissionInfo.FLAG_HARD_RESTRICTED
+ ) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime hard" +
+ " restricted permission that is not exempted, the actual permission flags" +
+ " $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testEvaluatePermissionState_restrictedPermissionsIsExempted_clearsRestrictionFlags() {
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionInfoFlags = PermissionInfo.FLAG_SOFT_RESTRICTED
+ ) {}
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.UPGRADE_EXEMPT
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime soft" +
+ " restricted permission that is exempted, the actual permission flags" +
+ " $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testInheritImplicitPermissionStates_runtimeExistingImplicit_sourceFlagsNotInherited() {
+ val oldImplicitPermissionFlags = PermissionFlags.USER_FIXED
+ testInheritImplicitPermissionStates(
+ implicitPermissionFlags = oldImplicitPermissionFlags,
+ isNewInstallAndNewPermission = false
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = oldImplicitPermissionFlags or PermissionFlags.IMPLICIT_GRANTED or
+ PermissionFlags.APP_OP_REVOKED
+ assertWithMessage(
+ "After $action is called for a package that requests a permission that is" +
+ " implicit, existing and runtime, it should not inherit the runtime flags from" +
+ " the source permission. Hence the actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testInheritImplicitPermissionStates_nonRuntimeNewImplicit_sourceFlagsNotInherited() {
+ testInheritImplicitPermissionStates(
+ implicitPermissionProtectionLevel = PermissionInfo.PROTECTION_NORMAL
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a permission that is" +
+ " implicit, new and non-runtime, it should not inherit the runtime flags from" +
+ " the source permission. Hence the actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testInheritImplicitPermissionStates_runtimeNewImplicitPermissions_sourceFlagsInherited() {
+ val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
+ testInheritImplicitPermissionStates(sourceRuntimeFlags = sourceRuntimeFlags)
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED or
+ PermissionFlags.IMPLICIT
+ assertWithMessage(
+ "After $action is called for a package that requests a permission that is" +
+ " implicit, new and runtime, it should inherit the runtime flags from" +
+ " the source permission. Hence the actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testInheritImplicitPermissionStates_grantingNewFromRevokeImplicit_onlyInheritFromSource() {
+ val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
+ testInheritImplicitPermissionStates(
+ implicitPermissionFlags = PermissionFlags.POLICY_FIXED,
+ sourceRuntimeFlags = sourceRuntimeFlags,
+ isAnySourcePermissionNonRuntime = false
+ )
+
+ val actualFlags = getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_NAME_0)
+ val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT
+ assertWithMessage(
+ "After $action is called for a package that requests a permission that is" +
+ " implicit, existing, runtime and revoked, it should only inherit runtime flags" +
+ " from source permission. Hence the actual permission flags $actualFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * If it's a media implicit permission (one of RETAIN_IMPLICIT_FLAGS_PERMISSIONS), we want to
+ * remove the IMPLICIT flag so that they will be granted when they are no longer implicit.
+ * (instead of revoking it)
+ */
+ @Test
+ fun testInheritImplicitPermissionStates_mediaImplicitPermissions_getsImplicitFlagRemoved() {
+ val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
+ testInheritImplicitPermissionStates(
+ implicitPermissionName = PERMISSION_ACCESS_MEDIA_LOCATION,
+ sourceRuntimeFlags = sourceRuntimeFlags
+ )
+
+ val actualFlags = getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_ACCESS_MEDIA_LOCATION
+ )
+ val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED
+ assertWithMessage(
+ "After $action is called for a package that requests a media permission that" +
+ " is implicit, new and runtime, it should inherit the runtime flags from" +
+ " the source permission and have the IMPLICIT flag removed. Hence the actual" +
+ " permission flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ private fun testInheritImplicitPermissionStates(
+ implicitPermissionName: String = PERMISSION_NAME_0,
+ implicitPermissionFlags: Int = 0,
+ implicitPermissionProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS,
+ sourceRuntimeFlags: Int = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET,
+ isAnySourcePermissionNonRuntime: Boolean = true,
+ isNewInstallAndNewPermission: Boolean = true
+ ) {
+ val userId = getUserIdEvaluated()
+ val implicitPermission = mockParsedPermission(
+ implicitPermissionName,
+ PACKAGE_NAME_0,
+ protectionLevel = implicitPermissionProtectionLevel,
+ )
+ // For source from non-runtime in order to grant by implicit
+ val sourcePermission1 = mockParsedPermission(
+ PERMISSION_NAME_1,
+ PACKAGE_NAME_0,
+ protectionLevel = if (isAnySourcePermissionNonRuntime) {
+ PermissionInfo.PROTECTION_NORMAL
+ } else {
+ PermissionInfo.PROTECTION_DANGEROUS
+ }
+ )
+ // For inheriting runtime flags
+ val sourcePermission2 = mockParsedPermission(
+ PERMISSION_NAME_2,
+ PACKAGE_NAME_0,
+ protectionLevel = PermissionInfo.PROTECTION_DANGEROUS,
+ )
+ val permissionOwnerPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(
+ PACKAGE_NAME_0,
+ permissions = listOf(implicitPermission, sourcePermission1, sourcePermission2)
+ )
+ )
+ val installedPackageState = mockPackageState(
+ APP_ID_1,
+ mockAndroidPackage(
+ PACKAGE_NAME_1,
+ requestedPermissions = setOf(
+ implicitPermissionName,
+ PERMISSION_NAME_1,
+ PERMISSION_NAME_2
+ ),
+ implicitPermissions = setOf(implicitPermissionName)
+ )
+ )
+ oldState.mutateExternalState().setImplicitToSourcePermissions(
+ MutableIndexedMap<String, IndexedListSet<String>>().apply {
+ put(implicitPermissionName, MutableIndexedListSet<String>().apply {
+ add(PERMISSION_NAME_1)
+ add(PERMISSION_NAME_2)
+ })
+ }
+ )
+ addPackageState(permissionOwnerPackageState)
+ addPermission(implicitPermission)
+ addPermission(sourcePermission1)
+ addPermission(sourcePermission2)
+ if (!isNewInstallAndNewPermission) {
+ addPackageState(installedPackageState)
+ setPermissionFlags(APP_ID_1, userId, implicitPermissionName, implicitPermissionFlags)
+ }
+ setPermissionFlags(APP_ID_1, userId, PERMISSION_NAME_2, sourceRuntimeFlags)
+
+ mutateState {
+ if (isNewInstallAndNewPermission) {
+ addPackageState(installedPackageState)
+ setPermissionFlags(
+ APP_ID_1,
+ userId,
+ implicitPermissionName,
+ implicitPermissionFlags,
+ newState
+ )
+ }
+ testAction(installedPackageState)
+ }
+ }
+
+ /**
+ * Setup simple package states for testing evaluatePermissionState().
+ * permissionOwnerPackageState is definer of permissionName with APP_ID_0.
+ * installedPackageState is the installed package that requests permissionName with APP_ID_1.
+ *
+ * @param oldFlags the existing permission flags for APP_ID_1, userId, permissionName
+ * @param protectionLevel the protectionLevel for the permission
+ * @param permissionName the name of the permission (1) being defined (2) of the oldFlags, and
+ * (3) requested by installedPackageState
+ * @param requestedPermissions the permissions requested by installedPackageState
+ * @param implicitPermissions the implicit permissions of installedPackageState
+ * @param permissionInfoFlags the flags for the permission itself
+ * @param isInstalledPackageSystem whether installedPackageState is a system package
+ *
+ * @return installedPackageState
+ */
+ private fun testEvaluatePermissionState(
+ oldFlags: Int,
+ protectionLevel: Int,
+ permissionName: String = PERMISSION_NAME_0,
+ requestedPermissions: Set<String> = setOf(permissionName),
+ implicitPermissions: Set<String> = emptySet(),
+ permissionInfoFlags: Int = 0,
+ isInstalledPackageSystem: Boolean = false,
+ isInstalledPackagePrivileged: Boolean = false,
+ isInstalledPackageProduct: Boolean = false,
+ isInstalledPackageSignatureMatching: Boolean = false,
+ isInstalledPackageVendor: Boolean = false,
+ installedPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+ isNewInstall: Boolean = false,
+ additionalSetup: () -> Unit
+ ) {
+ val userId = getUserIdEvaluated()
+ val parsedPermission = mockParsedPermission(
+ permissionName,
+ PACKAGE_NAME_0,
+ protectionLevel = protectionLevel,
+ flags = permissionInfoFlags
+ )
+ val permissionOwnerPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
+ )
+ val installedPackageState = mockPackageState(
+ APP_ID_1,
+ mockAndroidPackage(
+ PACKAGE_NAME_1,
+ requestedPermissions = requestedPermissions,
+ implicitPermissions = implicitPermissions,
+ targetSdkVersion = installedPackageTargetSdkVersion,
+ isSignatureMatching = isInstalledPackageSignatureMatching
+ ),
+ isSystem = isInstalledPackageSystem,
+ isPrivileged = isInstalledPackagePrivileged,
+ isProduct = isInstalledPackageProduct,
+ isVendor = isInstalledPackageVendor
+ )
+ addPackageState(permissionOwnerPackageState)
+ if (!isNewInstall) {
+ addPackageState(installedPackageState)
+ setPermissionFlags(APP_ID_1, userId, permissionName, oldFlags)
+ }
+ addPermission(parsedPermission)
+
+ additionalSetup()
+
+ mutateState {
+ if (isNewInstall) {
+ addPackageState(installedPackageState, newState)
+ setPermissionFlags(APP_ID_1, userId, permissionName, oldFlags, newState)
+ }
+ testAction(installedPackageState)
+ }
+ }
+
+ private fun getUserIdEvaluated(): Int = when (action) {
+ Action.ON_USER_ADDED -> USER_ID_NEW
+ Action.ON_STORAGE_VOLUME_ADDED, Action.ON_PACKAGE_ADDED -> USER_ID_0
+ }
+
+ private fun MutateStateScope.testAction(packageState: PackageState) {
+ with(appIdPermissionPolicy) {
+ when (action) {
+ Action.ON_USER_ADDED -> onUserAdded(getUserIdEvaluated())
+ Action.ON_STORAGE_VOLUME_ADDED -> onStorageVolumeMounted(
+ null,
+ listOf(packageState.packageName),
+ true
+ )
+ Action.ON_PACKAGE_ADDED -> onPackageAdded(packageState)
+ }
+ }
+ }
+
+ enum class Action { ON_USER_ADDED, ON_STORAGE_VOLUME_ADDED, ON_PACKAGE_ADDED }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun data(): Array<Action> = Action.values()
+ }
+}
\ No newline at end of file
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyTest.kt
deleted file mode 100644
index 3cf57a3..0000000
--- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyTest.kt
+++ /dev/null
@@ -1,1937 +0,0 @@
-/*
- * 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.server.permission.test
-
-import android.Manifest
-import android.content.pm.PackageManager
-import android.content.pm.PermissionGroupInfo
-import android.content.pm.PermissionInfo
-import android.content.pm.SigningDetails
-import android.os.Build
-import android.os.Bundle
-import android.util.ArrayMap
-import android.util.SparseArray
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.modules.utils.testing.ExtendedMockitoRule
-import com.android.server.extendedtestutils.wheneverStatic
-import com.android.server.permission.access.MutableAccessState
-import com.android.server.permission.access.MutableUserState
-import com.android.server.permission.access.MutateStateScope
-import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
-import com.android.server.permission.access.permission.AppIdPermissionPolicy
-import com.android.server.permission.access.permission.Permission
-import com.android.server.permission.access.permission.PermissionFlags
-import com.android.server.permission.access.util.hasBits
-import com.android.server.pm.parsing.PackageInfoUtils
-import com.android.server.pm.permission.PermissionAllowlist
-import com.android.server.pm.pkg.AndroidPackage
-import com.android.server.pm.pkg.PackageState
-import com.android.server.pm.pkg.PackageUserState
-import com.android.server.pm.pkg.component.ParsedPermission
-import com.android.server.pm.pkg.component.ParsedPermissionGroup
-import com.android.server.testutils.any
-import com.android.server.testutils.mock
-import com.android.server.testutils.whenever
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyLong
-
-/**
- * Mocking unit test for AppIdPermissionPolicy.
- */
-@RunWith(AndroidJUnit4::class)
-class AppIdPermissionPolicyTest {
- private lateinit var oldState: MutableAccessState
- private lateinit var newState: MutableAccessState
-
- private val defaultPermissionGroup = mockParsedPermissionGroup(
- PERMISSION_GROUP_NAME_0,
- PACKAGE_NAME_0
- )
- private val defaultPermissionTree = mockParsedPermission(
- PERMISSION_TREE_NAME,
- PACKAGE_NAME_0,
- isTree = true
- )
- private val defaultPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_0)
-
- private val appIdPermissionPolicy = AppIdPermissionPolicy()
-
- @Rule
- @JvmField
- val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
- .spyStatic(PackageInfoUtils::class.java)
- .build()
-
- @Before
- fun setUp() {
- oldState = MutableAccessState()
- createUserState(USER_ID_0)
- oldState.mutateExternalState().setPackageStates(ArrayMap())
- oldState.mutateExternalState().setDisabledSystemPackageStates(ArrayMap())
- mockPackageInfoUtilsGeneratePermissionInfo()
- mockPackageInfoUtilsGeneratePermissionGroupInfo()
- }
-
- private fun createUserState(userId: Int) {
- oldState.mutateExternalState().mutateUserIds().add(userId)
- oldState.mutateUserStatesNoWrite().put(userId, MutableUserState())
- }
-
- private fun mockPackageInfoUtilsGeneratePermissionInfo() {
- wheneverStatic {
- PackageInfoUtils.generatePermissionInfo(any(ParsedPermission::class.java), anyLong())
- }.thenAnswer { invocation ->
- val parsedPermission = invocation.getArgument<ParsedPermission>(0)
- val generateFlags = invocation.getArgument<Long>(1)
- PermissionInfo(parsedPermission.backgroundPermission).apply {
- name = parsedPermission.name
- packageName = parsedPermission.packageName
- metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) {
- parsedPermission.metaData
- } else {
- null
- }
- @Suppress("DEPRECATION")
- protectionLevel = parsedPermission.protectionLevel
- group = parsedPermission.group
- flags = parsedPermission.flags
- }
- }
- }
-
- private fun mockPackageInfoUtilsGeneratePermissionGroupInfo() {
- wheneverStatic {
- PackageInfoUtils.generatePermissionGroupInfo(
- any(ParsedPermissionGroup::class.java),
- anyLong()
- )
- }.thenAnswer { invocation ->
- val parsedPermissionGroup = invocation.getArgument<ParsedPermissionGroup>(0)
- val generateFlags = invocation.getArgument<Long>(1)
- @Suppress("DEPRECATION")
- PermissionGroupInfo().apply {
- name = parsedPermissionGroup.name
- packageName = parsedPermissionGroup.packageName
- metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) {
- parsedPermissionGroup.metaData
- } else {
- null
- }
- flags = parsedPermissionGroup.flags
- }
- }
- }
-
- @Test
- fun testResetRuntimePermissions_runtimeGranted_getsRevoked() {
- val oldFlags = PermissionFlags.RUNTIME_GRANTED
- val expectedNewFlags = 0
- testResetRuntimePermissions(oldFlags, expectedNewFlags)
- }
-
- @Test
- fun testResetRuntimePermissions_roleGranted_getsGranted() {
- val oldFlags = PermissionFlags.ROLE
- val expectedNewFlags = PermissionFlags.ROLE or PermissionFlags.RUNTIME_GRANTED
- testResetRuntimePermissions(oldFlags, expectedNewFlags)
- }
-
- @Test
- fun testResetRuntimePermissions_nullAndroidPackage_remainsUnchanged() {
- val oldFlags = PermissionFlags.RUNTIME_GRANTED
- val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
- testResetRuntimePermissions(oldFlags, expectedNewFlags, isAndroidPackageMissing = true)
- }
-
- private fun testResetRuntimePermissions(
- oldFlags: Int,
- expectedNewFlags: Int,
- isAndroidPackageMissing: Boolean = false
- ) {
- val parsedPermission = mockParsedPermission(
- PERMISSION_NAME_0,
- PACKAGE_NAME_0,
- protectionLevel = PermissionInfo.PROTECTION_DANGEROUS,
- )
- val permissionOwnerPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
- )
- val requestingPackageState = if (isAndroidPackageMissing) {
- mockPackageState(APP_ID_1, PACKAGE_NAME_1)
- } else {
- mockPackageState(
- APP_ID_1,
- mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0))
- )
- }
- setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags)
- addPackageState(permissionOwnerPackageState)
- addPackageState(requestingPackageState)
- addPermission(parsedPermission)
-
- mutateState {
- with(appIdPermissionPolicy) {
- resetRuntimePermissions(PACKAGE_NAME_1, USER_ID_0)
- }
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- assertWithMessage(
- "After resetting runtime permissions, permission flags did not match" +
- " expected values: expectedNewFlags is $expectedNewFlags," +
- " actualFlags is $actualFlags, while the oldFlags is $oldFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_permissionsOfMissingSystemApp_getsAdopted() {
- testAdoptPermissions(hasMissingPackage = true, isSystem = true)
-
- assertWithMessage(
- "After onPackageAdded() is called for a null adopt permission package," +
- " the permission package name: ${getPermission(PERMISSION_NAME_0)?.packageName}" +
- " did not match the expected package name: $PACKAGE_NAME_0"
- )
- .that(getPermission(PERMISSION_NAME_0)?.packageName)
- .isEqualTo(PACKAGE_NAME_0)
- }
-
- @Test
- fun testOnPackageAdded_permissionsOfExistingSystemApp_notAdopted() {
- testAdoptPermissions(isSystem = true)
-
- assertWithMessage(
- "After onPackageAdded() is called for a non-null adopt permission" +
- " package, the permission package name:" +
- " ${getPermission(PERMISSION_NAME_0)?.packageName} should not match the" +
- " package name: $PACKAGE_NAME_0"
- )
- .that(getPermission(PERMISSION_NAME_0)?.packageName)
- .isNotEqualTo(PACKAGE_NAME_0)
- }
-
- @Test
- fun testOnPackageAdded_permissionsOfNonSystemApp_notAdopted() {
- testAdoptPermissions(hasMissingPackage = true)
-
- assertWithMessage(
- "After onPackageAdded() is called for a non-system adopt permission" +
- " package, the permission package name:" +
- " ${getPermission(PERMISSION_NAME_0)?.packageName} should not match the" +
- " package name: $PACKAGE_NAME_0"
- )
- .that(getPermission(PERMISSION_NAME_0)?.packageName)
- .isNotEqualTo(PACKAGE_NAME_0)
- }
-
- private fun testAdoptPermissions(
- hasMissingPackage: Boolean = false,
- isSystem: Boolean = false
- ) {
- val parsedPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_1)
- val packageToAdoptPermission = if (hasMissingPackage) {
- mockPackageState(APP_ID_1, PACKAGE_NAME_1, isSystem = isSystem)
- } else {
- mockPackageState(
- APP_ID_1,
- mockAndroidPackage(
- PACKAGE_NAME_1,
- permissions = listOf(parsedPermission)
- ),
- isSystem = isSystem
- )
- }
- addPackageState(packageToAdoptPermission)
- addPermission(parsedPermission)
-
- mutateState {
- val installedPackage = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(
- PACKAGE_NAME_0,
- permissions = listOf(defaultPermission),
- adoptPermissions = listOf(PACKAGE_NAME_1)
- )
- )
- addPackageState(installedPackage, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(installedPackage)
- }
- }
- }
-
- @Test
- fun testOnPackageAdded_newPermissionGroup_getsDeclared() {
- mutateState {
- val packageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
- addPackageState(packageState, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(packageState)
- }
- }
-
- assertWithMessage(
- "After onPackageAdded() is called when there is no existing" +
- " permission groups, the new permission group $PERMISSION_GROUP_NAME_0 is not added"
- )
- .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.name)
- .isEqualTo(PERMISSION_GROUP_NAME_0)
- }
-
- @Test
- fun testOnPackageAdded_systemAppTakingOverPermissionGroupDefinition_getsTakenOver() {
- testTakingOverPermissionAndPermissionGroupDefinitions(newPermissionOwnerIsSystem = true)
-
- assertWithMessage(
- "After onPackageAdded() is called when $PERMISSION_GROUP_NAME_0 already" +
- " exists in the system, the system app $PACKAGE_NAME_0 didn't takeover the" +
- " ownership of this permission group"
- )
- .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
- .isEqualTo(PACKAGE_NAME_0)
- }
-
- @Test
- fun testOnPackageAdded_instantApps_remainsUnchanged() {
- testTakingOverPermissionAndPermissionGroupDefinitions(
- newPermissionOwnerIsInstant = true,
- permissionGroupAlreadyExists = false
- )
-
- assertWithMessage(
- "After onPackageAdded() is called for an instant app," +
- " the new permission group $PERMISSION_GROUP_NAME_0 should not be added"
- )
- .that(getPermissionGroup(PERMISSION_GROUP_NAME_0))
- .isNull()
- }
-
- @Test
- fun testOnPackageAdded_nonSystemAppTakingOverPermissionGroupDefinition_remainsUnchanged() {
- testTakingOverPermissionAndPermissionGroupDefinitions()
-
- assertWithMessage(
- "After onPackageAdded() is called when $PERMISSION_GROUP_NAME_0 already" +
- " exists in the system, non-system app $PACKAGE_NAME_0 shouldn't takeover" +
- " ownership of this permission group"
- )
- .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
- .isEqualTo(PACKAGE_NAME_1)
- }
-
- @Test
- fun testOnPackageAdded_takingOverPermissionGroupDeclaredBySystemApp_remainsUnchanged() {
- testTakingOverPermissionAndPermissionGroupDefinitions(oldPermissionOwnerIsSystem = true)
-
- assertWithMessage(
- "After onPackageAdded() is called when $PERMISSION_GROUP_NAME_0 already" +
- " exists in the system and is owned by a system app, app $PACKAGE_NAME_0" +
- " shouldn't takeover ownership of this permission group"
- )
- .that(getPermissionGroup(PERMISSION_GROUP_NAME_0)?.packageName)
- .isEqualTo(PACKAGE_NAME_1)
- }
-
- @Test
- fun testOnPackageAdded_newPermission_getsDeclared() {
- mutateState {
- val packageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
- addPackageState(packageState, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(packageState)
- }
- }
-
- assertWithMessage(
- "After onPackageAdded() is called when there is no existing" +
- " permissions, the new permission $PERMISSION_NAME_0 is not added"
- )
- .that(getPermission(PERMISSION_NAME_0)?.name)
- .isEqualTo(PERMISSION_NAME_0)
- }
-
- @Test
- fun testOnPackageAdded_configPermission_getsTakenOver() {
- testTakingOverPermissionAndPermissionGroupDefinitions(
- oldPermissionOwnerIsSystem = true,
- newPermissionOwnerIsSystem = true,
- type = Permission.TYPE_CONFIG,
- isReconciled = false
- )
-
- assertWithMessage(
- "After onPackageAdded() is called for a config permission with" +
- " no owner, the ownership is not taken over by a system app $PACKAGE_NAME_0"
- )
- .that(getPermission(PERMISSION_NAME_0)?.packageName)
- .isEqualTo(PACKAGE_NAME_0)
- }
-
- @Test
- fun testOnPackageAdded_systemAppTakingOverPermissionDefinition_getsTakenOver() {
- testTakingOverPermissionAndPermissionGroupDefinitions(newPermissionOwnerIsSystem = true)
-
- assertWithMessage(
- "After onPackageAdded() is called when $PERMISSION_NAME_0 already" +
- " exists in the system, the system app $PACKAGE_NAME_0 didn't takeover ownership" +
- " of this permission"
- )
- .that(getPermission(PERMISSION_NAME_0)?.packageName)
- .isEqualTo(PACKAGE_NAME_0)
- }
-
- @Test
- fun testOnPackageAdded_nonSystemAppTakingOverPermissionDefinition_remainsUnchanged() {
- testTakingOverPermissionAndPermissionGroupDefinitions()
-
- assertWithMessage(
- "After onPackageAdded() is called when $PERMISSION_NAME_0 already" +
- " exists in the system, the non-system app $PACKAGE_NAME_0 shouldn't takeover" +
- " ownership of this permission"
- )
- .that(getPermission(PERMISSION_NAME_0)?.packageName)
- .isEqualTo(PACKAGE_NAME_1)
- }
-
- @Test
- fun testOnPackageAdded_takingOverPermissionDeclaredBySystemApp_remainsUnchanged() {
- testTakingOverPermissionAndPermissionGroupDefinitions(oldPermissionOwnerIsSystem = true)
-
- assertWithMessage(
- "After onPackageAdded() is called when $PERMISSION_NAME_0 already" +
- " exists in system and is owned by a system app, the $PACKAGE_NAME_0 shouldn't" +
- " takeover ownership of this permission"
- )
- .that(getPermission(PERMISSION_NAME_0)?.packageName)
- .isEqualTo(PACKAGE_NAME_1)
- }
-
- private fun testTakingOverPermissionAndPermissionGroupDefinitions(
- oldPermissionOwnerIsSystem: Boolean = false,
- newPermissionOwnerIsSystem: Boolean = false,
- newPermissionOwnerIsInstant: Boolean = false,
- permissionGroupAlreadyExists: Boolean = true,
- permissionAlreadyExists: Boolean = true,
- type: Int = Permission.TYPE_MANIFEST,
- isReconciled: Boolean = true,
- ) {
- val oldPermissionOwnerPackageState = mockPackageState(
- APP_ID_1,
- PACKAGE_NAME_1,
- isSystem = oldPermissionOwnerIsSystem
- )
- addPackageState(oldPermissionOwnerPackageState)
- if (permissionGroupAlreadyExists) {
- addPermissionGroup(mockParsedPermissionGroup(PERMISSION_GROUP_NAME_0, PACKAGE_NAME_1))
- }
- if (permissionAlreadyExists) {
- addPermission(
- mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_1),
- type = type,
- isReconciled = isReconciled
- )
- }
-
- mutateState {
- val newPermissionOwnerPackageState = mockPackageState(
- APP_ID_0,
- mockSimpleAndroidPackage(),
- isSystem = newPermissionOwnerIsSystem,
- isInstantApp = newPermissionOwnerIsInstant
- )
- addPackageState(newPermissionOwnerPackageState, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(newPermissionOwnerPackageState)
- }
- }
- }
-
- @Test
- fun testOnPackageAdded_permissionGroupChanged_getsRevoked() {
- testPermissionChanged(
- oldPermissionGroup = PERMISSION_GROUP_NAME_1,
- newPermissionGroup = PERMISSION_GROUP_NAME_0
- )
-
- val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = 0
- assertWithMessage(
- "After onPackageAdded() is called for a package that has a permission group change" +
- " for a permission it defines, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_protectionLevelChanged_getsRevoked() {
- testPermissionChanged(newProtectionLevel = PermissionInfo.PROTECTION_INTERNAL)
-
- val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = 0
- assertWithMessage(
- "After onPackageAdded() is called for a package that has a protection level change" +
- " for a permission it defines, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- private fun testPermissionChanged(
- oldPermissionGroup: String? = null,
- newPermissionGroup: String? = null,
- newProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS
- ) {
- val oldPermission = mockParsedPermission(
- PERMISSION_NAME_0,
- PACKAGE_NAME_0,
- group = oldPermissionGroup,
- protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
- )
- val oldPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(oldPermission))
- )
- addPackageState(oldPackageState)
- addPermission(oldPermission)
- setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.RUNTIME_GRANTED)
-
- mutateState {
- val newPermission = mockParsedPermission(
- PERMISSION_NAME_0,
- PACKAGE_NAME_0,
- group = newPermissionGroup,
- protectionLevel = newProtectionLevel
- )
- val newPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(newPermission))
- )
- addPackageState(newPackageState, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(newPackageState)
- }
- }
- }
-
- @Test
- fun testOnPackageAdded_permissionTreeNoLongerDeclared_getsDefinitionRemoved() {
- testPermissionDeclaration {}
-
- assertWithMessage(
- "After onPackageAdded() is called for a package that no longer defines a permission" +
- " tree, the permission tree: $PERMISSION_NAME_0 in system state should be removed"
- )
- .that(getPermissionTree(PERMISSION_NAME_0))
- .isNull()
- }
-
- @Test
- fun testOnPackageAdded_permissionTreeByDisabledSystemPackage_remainsUnchanged() {
- testPermissionDeclaration {
- val disabledSystemPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
- addDisabledSystemPackageState(disabledSystemPackageState)
- }
-
- assertWithMessage(
- "After onPackageAdded() is called for a package that no longer defines" +
- " a permission tree while this permission tree is still defined by" +
- " a disabled system package, the permission tree: $PERMISSION_NAME_0 in" +
- " system state should not be removed"
- )
- .that(getPermissionTree(PERMISSION_TREE_NAME))
- .isNotNull()
- }
-
- @Test
- fun testOnPackageAdded_permissionNoLongerDeclared_getsDefinitionRemoved() {
- testPermissionDeclaration {}
-
- assertWithMessage(
- "After onPackageAdded() is called for a package that no longer defines a permission," +
- " the permission: $PERMISSION_NAME_0 in system state should be removed"
- )
- .that(getPermission(PERMISSION_NAME_0))
- .isNull()
- }
-
- @Test
- fun testOnPackageAdded_permissionByDisabledSystemPackage_remainsUnchanged() {
- testPermissionDeclaration {
- val disabledSystemPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
- addDisabledSystemPackageState(disabledSystemPackageState)
- }
-
- assertWithMessage(
- "After onPackageAdded() is called for a disabled system package and it's updated apk" +
- " no longer defines a permission, the permission: $PERMISSION_NAME_0 in" +
- " system state should not be removed"
- )
- .that(getPermission(PERMISSION_NAME_0))
- .isNotNull()
- }
-
- private fun testPermissionDeclaration(additionalSetup: () -> Unit) {
- val oldPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
- addPackageState(oldPackageState)
- addPermission(defaultPermissionTree)
- addPermission(defaultPermission)
-
- additionalSetup()
-
- mutateState {
- val newPackageState = mockPackageState(APP_ID_0, mockAndroidPackage(PACKAGE_NAME_0))
- addPackageState(newPackageState, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(newPackageState)
- }
- }
- }
-
- @Test
- fun testOnPackageAdded_permissionsNoLongerRequested_getsFlagsRevoked() {
- val parsedPermission = mockParsedPermission(
- PERMISSION_NAME_0,
- PACKAGE_NAME_0,
- protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
- )
- val oldPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(
- PACKAGE_NAME_0,
- permissions = listOf(parsedPermission),
- requestedPermissions = setOf(PERMISSION_NAME_0)
- )
- )
- addPackageState(oldPackageState)
- addPermission(parsedPermission)
- setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.RUNTIME_GRANTED)
-
- mutateState {
- val newPackageState = mockPackageState(APP_ID_0, mockSimpleAndroidPackage())
- addPackageState(newPackageState, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(newPackageState)
- }
- }
-
- val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = 0
- assertWithMessage(
- "After onPackageAdded() is called for a package that no longer requests a permission" +
- " the actual permission flags $actualFlags should match the" +
- " expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_storageAndMediaPermissionsDowngradingPastQ_getsRuntimeRevoked() {
- testRevokePermissionsOnPackageUpdate(
- PermissionFlags.RUNTIME_GRANTED,
- newTargetSdkVersion = Build.VERSION_CODES.P
- )
-
- val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
- val expectedNewFlags = 0
- assertWithMessage(
- "After onPackageAdded() is called for a package that's downgrading past Q" +
- " the actual permission flags $actualFlags should match the" +
- " expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_storageAndMediaPermissionsNotDowngradingPastQ_remainsUnchanged() {
- val oldFlags = PermissionFlags.RUNTIME_GRANTED
- testRevokePermissionsOnPackageUpdate(
- oldFlags,
- oldTargetSdkVersion = Build.VERSION_CODES.P,
- newTargetSdkVersion = Build.VERSION_CODES.P
- )
-
- val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that's not downgrading past Q" +
- " the actual permission flags $actualFlags should match the" +
- " expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_policyFixedDowngradingPastQ_remainsUnchanged() {
- val oldFlags = PermissionFlags.RUNTIME_GRANTED and PermissionFlags.POLICY_FIXED
- testRevokePermissionsOnPackageUpdate(oldFlags, newTargetSdkVersion = Build.VERSION_CODES.P)
-
- val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that's downgrading past Q" +
- " the actual permission flags with PermissionFlags.POLICY_FIXED $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_newlyRequestingLegacyExternalStorage_getsRuntimeRevoked() {
- testRevokePermissionsOnPackageUpdate(
- PermissionFlags.RUNTIME_GRANTED,
- oldTargetSdkVersion = Build.VERSION_CODES.P,
- newTargetSdkVersion = Build.VERSION_CODES.P,
- oldIsRequestLegacyExternalStorage = false
- )
-
- val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
- val expectedNewFlags = 0
- assertWithMessage(
- "After onPackageAdded() is called for a package with" +
- " newlyRequestingLegacyExternalStorage, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_missingOldPackage_remainsUnchanged() {
- val oldFlags = PermissionFlags.RUNTIME_GRANTED
- testRevokePermissionsOnPackageUpdate(
- oldFlags,
- newTargetSdkVersion = Build.VERSION_CODES.P,
- isOldPackageMissing = true
- )
-
- val actualFlags = getPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that's downgrading past Q" +
- " and doesn't have the oldPackage, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- private fun testRevokePermissionsOnPackageUpdate(
- oldFlags: Int,
- oldTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
- newTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
- oldIsRequestLegacyExternalStorage: Boolean = true,
- newIsRequestLegacyExternalStorage: Boolean = true,
- isOldPackageMissing: Boolean = false
- ) {
- val parsedPermission = mockParsedPermission(
- PERMISSION_READ_EXTERNAL_STORAGE,
- PACKAGE_NAME_0,
- protectionLevel = PermissionInfo.PROTECTION_DANGEROUS
- )
- val oldPackageState = if (isOldPackageMissing) {
- mockPackageState(APP_ID_0, PACKAGE_NAME_0)
- } else {
- mockPackageState(
- APP_ID_0,
- mockAndroidPackage(
- PACKAGE_NAME_0,
- targetSdkVersion = oldTargetSdkVersion,
- isRequestLegacyExternalStorage = oldIsRequestLegacyExternalStorage,
- requestedPermissions = setOf(PERMISSION_READ_EXTERNAL_STORAGE),
- permissions = listOf(parsedPermission)
- )
- )
- }
- addPackageState(oldPackageState)
- addPermission(parsedPermission)
- setPermissionFlags(APP_ID_0, USER_ID_0, PERMISSION_READ_EXTERNAL_STORAGE, oldFlags)
-
- mutateState {
- val newPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(
- PACKAGE_NAME_0,
- targetSdkVersion = newTargetSdkVersion,
- isRequestLegacyExternalStorage = newIsRequestLegacyExternalStorage,
- requestedPermissions = setOf(PERMISSION_READ_EXTERNAL_STORAGE),
- permissions = listOf(parsedPermission)
- )
- )
- addPackageState(newPackageState, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(newPackageState)
- }
- }
- }
-
- @Test
- fun testOnPackageAdded_normalPermissionAlreadyGranted_remainsUnchanged() {
- val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.INSTALL_REVOKED
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_NORMAL) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a normal permission" +
- " with an existing INSTALL_GRANTED flag, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_normalPermissionNotInstallRevoked_getsGranted() {
- val oldFlags = 0
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_NORMAL,
- isNewInstall = true
- ) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a normal permission" +
- " with no existing flags, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_normalPermissionRequestedByInstalledPackage_getsGranted() {
- val oldFlags = PermissionFlags.INSTALL_REVOKED
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_NORMAL) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a normal permission" +
- " with the INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags since it's a new install"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- /**
- * We setup a permission protection level change from SIGNATURE to NORMAL in order to make
- * the permission a "changed permission" in order to test evaluatePermissionState() called by
- * evaluatePermissionStateForAllPackages(). This makes the requestingPackageState not the
- * installedPackageState so that we can test whether requesting by system package will give us
- * the expected permission flags.
- *
- * Besides, this also helps us test evaluatePermissionStateForAllPackages(). Since both
- * evaluatePermissionStateForAllPackages() and evaluateAllPermissionStatesForPackage() call
- * evaluatePermissionState() in their implementations, we use these tests as the only tests
- * that test evaluatePermissionStateForAllPackages()
- */
- @Test
- fun testOnPackageAdded_normalPermissionRequestedBySystemPackage_getsGranted() {
- testEvaluateNormalPermissionStateWithPermissionChanges(requestingPackageIsSystem = true)
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a system package that requests a normal" +
- " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_normalCompatibilityPermission_getsGranted() {
- testEvaluateNormalPermissionStateWithPermissionChanges(
- permissionName = PERMISSION_POST_NOTIFICATIONS,
- requestingPackageTargetSdkVersion = Build.VERSION_CODES.S
- )
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_POST_NOTIFICATIONS)
- val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a normal compatibility" +
- " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_normalPermissionPreviouslyRevoked_getsInstallRevoked() {
- testEvaluateNormalPermissionStateWithPermissionChanges()
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.INSTALL_REVOKED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a normal" +
- " permission with INSTALL_REVOKED flag, the actual permission flags $actualFlags" +
- " should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- private fun testEvaluateNormalPermissionStateWithPermissionChanges(
- permissionName: String = PERMISSION_NAME_0,
- requestingPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
- requestingPackageIsSystem: Boolean = false
- ) {
- val oldParsedPermission = mockParsedPermission(
- permissionName,
- PACKAGE_NAME_0,
- protectionLevel = PermissionInfo.PROTECTION_SIGNATURE
- )
- val oldPermissionOwnerPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(oldParsedPermission))
- )
- val requestingPackageState = mockPackageState(
- APP_ID_1,
- mockAndroidPackage(
- PACKAGE_NAME_1,
- requestedPermissions = setOf(permissionName),
- targetSdkVersion = requestingPackageTargetSdkVersion
- ),
- isSystem = requestingPackageIsSystem,
- )
- addPackageState(oldPermissionOwnerPackageState)
- addPackageState(requestingPackageState)
- addPermission(oldParsedPermission)
- val oldFlags = PermissionFlags.INSTALL_REVOKED
- setPermissionFlags(APP_ID_1, USER_ID_0, permissionName, oldFlags)
-
- mutateState {
- val newPermissionOwnerPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(
- PACKAGE_NAME_0,
- permissions = listOf(mockParsedPermission(permissionName, PACKAGE_NAME_0))
- )
- )
- addPackageState(newPermissionOwnerPackageState, newState)
- with(appIdPermissionPolicy) {
- onPackageAdded(newPermissionOwnerPackageState)
- }
- }
- }
-
- @Test
- fun testOnPackageAdded_normalAppOpPermission_getsRoleAndUserSetFlagsPreserved() {
- val oldFlags = PermissionFlags.ROLE or PermissionFlags.USER_SET
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_NORMAL or PermissionInfo.PROTECTION_FLAG_APPOP
- ) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.INSTALL_GRANTED or oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a normal app op" +
- " permission with existing ROLE and USER_SET flags, the actual permission flags" +
- " $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_internalPermissionWasGrantedWithMissingPackage_getsProtectionGranted() {
- val oldFlags = PermissionFlags.PROTECTION_GRANTED
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_INTERNAL) {
- val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
- addPackageState(packageStateWithMissingPackage)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests an internal permission" +
- " with missing android package and $oldFlags flag, the actual permission flags" +
- " $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_internalAppOpPermission_getsRoleAndUserSetFlagsPreserved() {
- val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or
- PermissionFlags.USER_SET
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_APPOP
- ) {
- val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
- addPackageState(packageStateWithMissingPackage)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests an internal permission" +
- " with missing android package and $oldFlags flag and the permission isAppOp," +
- " the actual permission flags $actualFlags should match the expected" +
- " flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_internalDevelopmentPermission_getsRuntimeGrantedPreserved() {
- val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.RUNTIME_GRANTED
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_DEVELOPMENT
- ) {
- val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
- addPackageState(packageStateWithMissingPackage)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests an internal permission" +
- " with missing android package and $oldFlags flag and permission isDevelopment," +
- " the actual permission flags $actualFlags should match the expected" +
- " flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_internalRolePermission_getsRoleAndRuntimeGrantedPreserved() {
- val oldFlags = PermissionFlags.PROTECTION_GRANTED or PermissionFlags.ROLE or
- PermissionFlags.RUNTIME_GRANTED
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_INTERNAL or PermissionInfo.PROTECTION_FLAG_ROLE
- ) {
- val packageStateWithMissingPackage = mockPackageState(APP_ID_1, MISSING_ANDROID_PACKAGE)
- addPackageState(packageStateWithMissingPackage)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests an internal permission" +
- " with missing android package and $oldFlags flag and the permission isRole," +
- " the actual permission flags $actualFlags should match the expected" +
- " flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_signaturePrivilegedPermissionNotAllowlisted_isNotGranted() {
- val oldFlags = 0
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_SIGNATURE or PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
- isInstalledPackageSystem = true,
- isInstalledPackagePrivileged = true,
- isInstalledPackageProduct = true,
- // To mock the return value of shouldGrantPrivilegedOrOemPermission()
- isInstalledPackageVendor = true,
- isNewInstall = true
- ) {
- val platformPackage = mockPackageState(
- PLATFORM_APP_ID,
- mockAndroidPackage(PLATFORM_PACKAGE_NAME)
- )
- setupAllowlist(PACKAGE_NAME_1, false)
- addPackageState(platformPackage)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a signature privileged" +
- " permission that's not allowlisted, the actual permission" +
- " flags $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_nonPrivilegedPermissionShouldGrantBySignature_getsProtectionGranted() {
- val oldFlags = 0
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_SIGNATURE,
- isInstalledPackageSystem = true,
- isInstalledPackagePrivileged = true,
- isInstalledPackageProduct = true,
- isInstalledPackageSignatureMatching = true,
- isInstalledPackageVendor = true,
- isNewInstall = true
- ) {
- val platformPackage = mockPackageState(
- PLATFORM_APP_ID,
- mockAndroidPackage(PLATFORM_PACKAGE_NAME, isSignatureMatching = true)
- )
- setupAllowlist(PACKAGE_NAME_1, false)
- addPackageState(platformPackage)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a signature" +
- " non-privileged permission, the actual permission" +
- " flags $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_privilegedAllowlistPermissionShouldGrantByProtectionFlags_getsGranted() {
- val oldFlags = 0
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_SIGNATURE or PermissionInfo.PROTECTION_FLAG_PRIVILEGED,
- isInstalledPackageSystem = true,
- isInstalledPackagePrivileged = true,
- isInstalledPackageProduct = true,
- isNewInstall = true
- ) {
- val platformPackage = mockPackageState(
- PLATFORM_APP_ID,
- mockAndroidPackage(PLATFORM_PACKAGE_NAME)
- )
- setupAllowlist(PACKAGE_NAME_1, true)
- addPackageState(platformPackage)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.PROTECTION_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a signature privileged" +
- " permission that's allowlisted and should grant by protection flags, the actual" +
- " permission flags $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- private fun setupAllowlist(
- packageName: String,
- allowlistState: Boolean,
- state: MutableAccessState = oldState
- ) {
- state.mutateExternalState().setPrivilegedPermissionAllowlistPackages(
- MutableIndexedListSet<String>().apply { add(packageName) }
- )
- val mockAllowlist = mock<PermissionAllowlist> {
- whenever(
- getProductPrivilegedAppAllowlistState(packageName, PERMISSION_NAME_0)
- ).thenReturn(allowlistState)
- }
- state.mutateExternalState().setPermissionAllowlist(mockAllowlist)
- }
-
- @Test
- fun testOnPackageAdded_nonRuntimeFlagsOnRuntimePermissions_getsCleared() {
- val oldFlags = PermissionFlags.INSTALL_GRANTED or PermissionFlags.PREGRANT or
- PermissionFlags.RUNTIME_GRANTED
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.PREGRANT or PermissionFlags.RUNTIME_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime permission" +
- " with existing $oldFlags flags, the actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_newPermissionsForPreM_requiresUserReview() {
- val oldFlags = 0
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_DANGEROUS,
- installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP,
- isNewInstall = true
- ) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime permission" +
- " with no existing flags in pre M, actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_legacyOrImplicitGrantedPermissionPreviouslyRevoked_getsAppOpRevoked() {
- val oldFlags = PermissionFlags.USER_FIXED
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_DANGEROUS,
- installedPackageTargetSdkVersion = Build.VERSION_CODES.LOLLIPOP
- ) {
- setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.USER_FIXED or
- PermissionFlags.APP_OP_REVOKED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime permission" +
- " that should be LEGACY_GRANTED or IMPLICIT_GRANTED that was previously revoked," +
- " the actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_legacyGrantedPermissionsForPostM_userReviewRequirementRemoved() {
- val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.IMPLICIT
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = 0
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime permission" +
- " that used to require user review, the user review requirement should be removed" +
- " if it's upgraded to post M. The actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_legacyGrantedPermissionsAlreadyReviewedForPostM_getsGranted() {
- val oldFlags = PermissionFlags.LEGACY_GRANTED
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime permission" +
- " that was already reviewed by the user, the permission should be RUNTIME_GRANTED" +
- " if it's upgraded to post M. The actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_leanbackNotificationPermissionsForPostM_getsImplicitGranted() {
- val oldFlags = 0
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_DANGEROUS,
- permissionName = PERMISSION_POST_NOTIFICATIONS,
- isNewInstall = true
- ) {
- oldState.mutateExternalState().setLeanback(true)
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_POST_NOTIFICATIONS)
- val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime notification" +
- " permission when isLeanback, the actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_implicitSourceFromNonRuntime_getsImplicitGranted() {
- val oldFlags = 0
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_DANGEROUS,
- implicitPermissions = setOf(PERMISSION_NAME_0),
- isNewInstall = true
- ) {
- oldState.mutateExternalState().setImplicitToSourcePermissions(
- MutableIndexedMap<String, IndexedListSet<String>>().apply {
- put(PERMISSION_NAME_0, MutableIndexedListSet<String>().apply {
- add(PERMISSION_NAME_1)
- })
- }
- )
- addPermission(mockParsedPermission(PERMISSION_NAME_1, PACKAGE_NAME_0))
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime implicit" +
- " permission that's source from a non-runtime permission, the actual permission" +
- " flags $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- /**
- * For a legacy granted or implicit permission during the app upgrade, when the permission
- * should no longer be legacy or implicit granted, we want to remove the APP_OP_REVOKED flag
- * so that the app can request the permission.
- */
- @Test
- fun testOnPackageAdded_noLongerLegacyOrImplicitGranted_canBeRequested() {
- val oldFlags = PermissionFlags.LEGACY_GRANTED or PermissionFlags.APP_OP_REVOKED or
- PermissionFlags.RUNTIME_GRANTED
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = 0
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime permission" +
- " that is no longer LEGACY_GRANTED or IMPLICIT_GRANTED, the actual permission" +
- " flags $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_noLongerImplicitPermissions_getsRuntimeAndImplicitFlagsRemoved() {
- val oldFlags = PermissionFlags.IMPLICIT or PermissionFlags.RUNTIME_GRANTED or
- PermissionFlags.USER_SET or PermissionFlags.USER_FIXED
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = 0
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime permission" +
- " that is no longer implicit and we shouldn't retain as nearby device" +
- " permissions, the actual permission flags $actualFlags should match the expected" +
- " flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_noLongerImplicitNearbyPermissionsWasGranted_getsRuntimeGranted() {
- val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_DANGEROUS,
- permissionName = PERMISSION_BLUETOOTH_CONNECT,
- requestedPermissions = setOf(
- PERMISSION_BLUETOOTH_CONNECT,
- PERMISSION_ACCESS_BACKGROUND_LOCATION
- )
- ) {
- setPermissionFlags(
- APP_ID_1,
- USER_ID_0,
- PERMISSION_ACCESS_BACKGROUND_LOCATION,
- PermissionFlags.RUNTIME_GRANTED
- )
- }
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_BLUETOOTH_CONNECT)
- val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime nearby device" +
- " permission that was granted by implicit, the actual permission flags" +
- " $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_noLongerImplicitSystemOrPolicyFixedWasGranted_getsRuntimeGranted() {
- val oldFlags = PermissionFlags.IMPLICIT_GRANTED or PermissionFlags.IMPLICIT or
- PermissionFlags.SYSTEM_FIXED
- testEvaluatePermissionState(oldFlags, PermissionInfo.PROTECTION_DANGEROUS) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.SYSTEM_FIXED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime permission" +
- " that was granted and is no longer implicit and is SYSTEM_FIXED or POLICY_FIXED," +
- " the actual permission flags $actualFlags should match the expected" +
- " flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_restrictedPermissionsNotExempt_getsRestrictionFlags() {
- val oldFlags = PermissionFlags.RESTRICTION_REVOKED
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_DANGEROUS,
- permissionInfoFlags = PermissionInfo.FLAG_HARD_RESTRICTED
- ) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = oldFlags
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime hard" +
- " restricted permission that is not exempted, the actual permission flags" +
- " $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_restrictedPermissionsIsExempted_clearsRestrictionFlags() {
- val oldFlags = 0
- testEvaluatePermissionState(
- oldFlags,
- PermissionInfo.PROTECTION_DANGEROUS,
- permissionInfoFlags = PermissionInfo.FLAG_SOFT_RESTRICTED
- ) {}
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.UPGRADE_EXEMPT
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a runtime soft" +
- " restricted permission that is exempted, the actual permission flags" +
- " $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_runtimeExistingImplicitPermissions_sourceFlagsNotInherited() {
- val oldImplicitPermissionFlags = PermissionFlags.USER_FIXED
- testInheritImplicitPermissionStates(
- implicitPermissionFlags = oldImplicitPermissionFlags,
- isNewInstallAndNewPermission = false
- )
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = oldImplicitPermissionFlags or PermissionFlags.IMPLICIT_GRANTED or
- PermissionFlags.APP_OP_REVOKED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a permission that is" +
- " implicit, existing and runtime, it should not inherit the runtime flags from" +
- " the source permission. Hence the actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_nonRuntimeNewImplicitPermissions_sourceFlagsNotInherited() {
- testInheritImplicitPermissionStates(
- implicitPermissionProtectionLevel = PermissionInfo.PROTECTION_NORMAL
- )
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = PermissionFlags.INSTALL_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a permission that is" +
- " implicit, new and non-runtime, it should not inherit the runtime flags from" +
- " the source permission. Hence the actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_runtimeNewImplicitPermissions_sourceFlagsInherited() {
- val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
- testInheritImplicitPermissionStates(sourceRuntimeFlags = sourceRuntimeFlags)
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED or
- PermissionFlags.IMPLICIT
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a permission that is" +
- " implicit, new and runtime, it should inherit the runtime flags from" +
- " the source permission. Hence the actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- @Test
- fun testOnPackageAdded_grantingNewFromRevokeImplicitPermissions_onlySourceFlagsInherited() {
- val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
- testInheritImplicitPermissionStates(
- implicitPermissionFlags = PermissionFlags.POLICY_FIXED,
- sourceRuntimeFlags = sourceRuntimeFlags,
- isAnySourcePermissionNonRuntime = false
- )
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
- val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a permission that is" +
- " implicit, existing, runtime and revoked, it should only inherit runtime flags" +
- " from source permission. Hence the actual permission flags $actualFlags should" +
- " match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- /**
- * If it's a media implicit permission (one of RETAIN_IMPLICIT_FLAGS_PERMISSIONS), we want to
- * remove the IMPLICIT flag so that they will be granted when they are no longer implicit.
- * (instead of revoking it)
- */
- @Test
- fun testOnPackageAdded_mediaImplicitPermissions_getsImplicitFlagRemoved() {
- val sourceRuntimeFlags = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET
- testInheritImplicitPermissionStates(
- implicitPermissionName = PERMISSION_ACCESS_MEDIA_LOCATION,
- sourceRuntimeFlags = sourceRuntimeFlags
- )
-
- val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_ACCESS_MEDIA_LOCATION)
- val expectedNewFlags = sourceRuntimeFlags or PermissionFlags.IMPLICIT_GRANTED
- assertWithMessage(
- "After onPackageAdded() is called for a package that requests a media permission that" +
- " is implicit, new and runtime, it should inherit the runtime flags from" +
- " the source permission and have the IMPLICIT flag removed. Hence the actual" +
- " permission flags $actualFlags should match the expected flags $expectedNewFlags"
- )
- .that(actualFlags)
- .isEqualTo(expectedNewFlags)
- }
-
- private fun testInheritImplicitPermissionStates(
- implicitPermissionName: String = PERMISSION_NAME_0,
- implicitPermissionFlags: Int = 0,
- implicitPermissionProtectionLevel: Int = PermissionInfo.PROTECTION_DANGEROUS,
- sourceRuntimeFlags: Int = PermissionFlags.RUNTIME_GRANTED or PermissionFlags.USER_SET,
- isAnySourcePermissionNonRuntime: Boolean = true,
- isNewInstallAndNewPermission: Boolean = true
- ) {
- val implicitPermission = mockParsedPermission(
- implicitPermissionName,
- PACKAGE_NAME_0,
- protectionLevel = implicitPermissionProtectionLevel,
- )
- // For source from non-runtime in order to grant by implicit
- val sourcePermission1 = mockParsedPermission(
- PERMISSION_NAME_1,
- PACKAGE_NAME_0,
- protectionLevel = if (isAnySourcePermissionNonRuntime) {
- PermissionInfo.PROTECTION_NORMAL
- } else {
- PermissionInfo.PROTECTION_DANGEROUS
- }
- )
- // For inheriting runtime flags
- val sourcePermission2 = mockParsedPermission(
- PERMISSION_NAME_2,
- PACKAGE_NAME_0,
- protectionLevel = PermissionInfo.PROTECTION_DANGEROUS,
- )
- val permissionOwnerPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(
- PACKAGE_NAME_0,
- permissions = listOf(implicitPermission, sourcePermission1, sourcePermission2)
- )
- )
- val installedPackageState = mockPackageState(
- APP_ID_1,
- mockAndroidPackage(
- PACKAGE_NAME_1,
- requestedPermissions = setOf(
- implicitPermissionName,
- PERMISSION_NAME_1,
- PERMISSION_NAME_2
- ),
- implicitPermissions = setOf(implicitPermissionName)
- )
- )
- oldState.mutateExternalState().setImplicitToSourcePermissions(
- MutableIndexedMap<String, IndexedListSet<String>>().apply {
- put(implicitPermissionName, MutableIndexedListSet<String>().apply {
- add(PERMISSION_NAME_1)
- add(PERMISSION_NAME_2)
- })
- }
- )
- addPackageState(permissionOwnerPackageState)
- addPermission(implicitPermission)
- addPermission(sourcePermission1)
- addPermission(sourcePermission2)
- if (!isNewInstallAndNewPermission) {
- addPackageState(installedPackageState)
- setPermissionFlags(APP_ID_1, USER_ID_0, implicitPermissionName, implicitPermissionFlags)
- }
- setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_2, sourceRuntimeFlags)
-
- mutateState {
- if (isNewInstallAndNewPermission) {
- addPackageState(installedPackageState)
- setPermissionFlags(
- APP_ID_1,
- USER_ID_0,
- implicitPermissionName,
- implicitPermissionFlags,
- newState
- )
- }
- with(appIdPermissionPolicy) {
- onPackageAdded(installedPackageState)
- }
- }
- }
-
- /**
- * Setup simple package states for testing evaluatePermissionState().
- * permissionOwnerPackageState is definer of permissionName with APP_ID_0.
- * installedPackageState is the installed package that requests permissionName with APP_ID_1.
- *
- * @param oldFlags the existing permission flags for APP_ID_1, USER_ID_0, permissionName
- * @param protectionLevel the protectionLevel for the permission
- * @param permissionName the name of the permission (1) being defined (2) of the oldFlags, and
- * (3) requested by installedPackageState
- * @param requestedPermissions the permissions requested by installedPackageState
- * @param implicitPermissions the implicit permissions of installedPackageState
- * @param permissionInfoFlags the flags for the permission itself
- * @param isInstalledPackageSystem whether installedPackageState is a system package
- *
- * @return installedPackageState
- */
- fun testEvaluatePermissionState(
- oldFlags: Int,
- protectionLevel: Int,
- permissionName: String = PERMISSION_NAME_0,
- requestedPermissions: Set<String> = setOf(permissionName),
- implicitPermissions: Set<String> = emptySet(),
- permissionInfoFlags: Int = 0,
- isInstalledPackageSystem: Boolean = false,
- isInstalledPackagePrivileged: Boolean = false,
- isInstalledPackageProduct: Boolean = false,
- isInstalledPackageSignatureMatching: Boolean = false,
- isInstalledPackageVendor: Boolean = false,
- installedPackageTargetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
- isNewInstall: Boolean = false,
- additionalSetup: () -> Unit
- ) {
- val parsedPermission = mockParsedPermission(
- permissionName,
- PACKAGE_NAME_0,
- protectionLevel = protectionLevel,
- flags = permissionInfoFlags
- )
- val permissionOwnerPackageState = mockPackageState(
- APP_ID_0,
- mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
- )
- val installedPackageState = mockPackageState(
- APP_ID_1,
- mockAndroidPackage(
- PACKAGE_NAME_1,
- requestedPermissions = requestedPermissions,
- implicitPermissions = implicitPermissions,
- targetSdkVersion = installedPackageTargetSdkVersion,
- isSignatureMatching = isInstalledPackageSignatureMatching
- ),
- isSystem = isInstalledPackageSystem,
- isPrivileged = isInstalledPackagePrivileged,
- isProduct = isInstalledPackageProduct,
- isVendor = isInstalledPackageVendor
- )
- addPackageState(permissionOwnerPackageState)
- if (!isNewInstall) {
- addPackageState(installedPackageState)
- setPermissionFlags(APP_ID_1, USER_ID_0, permissionName, oldFlags)
- }
- addPermission(parsedPermission)
-
- additionalSetup()
-
- mutateState {
- if (isNewInstall) {
- addPackageState(installedPackageState, newState)
- setPermissionFlags(APP_ID_1, USER_ID_0, permissionName, oldFlags, newState)
- }
- with(appIdPermissionPolicy) {
- onPackageAdded(installedPackageState)
- }
- }
- }
-
- /**
- * Mock an AndroidPackage with PACKAGE_NAME_0, PERMISSION_NAME_0 and PERMISSION_GROUP_NAME_0
- */
- private fun mockSimpleAndroidPackage(): AndroidPackage =
- mockAndroidPackage(
- PACKAGE_NAME_0,
- permissionGroups = listOf(defaultPermissionGroup),
- permissions = listOf(defaultPermissionTree, defaultPermission)
- )
-
- private inline fun mutateState(action: MutateStateScope.() -> Unit) {
- newState = oldState.toMutable()
- MutateStateScope(oldState, newState).action()
- }
-
- private fun mockPackageState(
- appId: Int,
- packageName: String,
- isSystem: Boolean = false,
- ): PackageState =
- mock {
- whenever(this.appId).thenReturn(appId)
- whenever(this.packageName).thenReturn(packageName)
- whenever(androidPackage).thenReturn(null)
- whenever(this.isSystem).thenReturn(isSystem)
- }
-
- private fun mockPackageState(
- appId: Int,
- androidPackage: AndroidPackage,
- isSystem: Boolean = false,
- isPrivileged: Boolean = false,
- isProduct: Boolean = false,
- isInstantApp: Boolean = false,
- isVendor: Boolean = false
- ): PackageState =
- mock {
- whenever(this.appId).thenReturn(appId)
- whenever(this.androidPackage).thenReturn(androidPackage)
- val packageName = androidPackage.packageName
- whenever(this.packageName).thenReturn(packageName)
- whenever(this.isSystem).thenReturn(isSystem)
- whenever(this.isPrivileged).thenReturn(isPrivileged)
- whenever(this.isProduct).thenReturn(isProduct)
- whenever(this.isVendor).thenReturn(isVendor)
- val userStates = SparseArray<PackageUserState>().apply {
- put(USER_ID_0, mock { whenever(this.isInstantApp).thenReturn(isInstantApp) })
- }
- whenever(this.userStates).thenReturn(userStates)
- }
-
- private fun mockAndroidPackage(
- packageName: String,
- targetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
- isRequestLegacyExternalStorage: Boolean = false,
- adoptPermissions: List<String> = emptyList(),
- implicitPermissions: Set<String> = emptySet(),
- requestedPermissions: Set<String> = emptySet(),
- permissionGroups: List<ParsedPermissionGroup> = emptyList(),
- permissions: List<ParsedPermission> = emptyList(),
- isSignatureMatching: Boolean = false
- ): AndroidPackage =
- mock {
- whenever(this.packageName).thenReturn(packageName)
- whenever(this.targetSdkVersion).thenReturn(targetSdkVersion)
- whenever(this.isRequestLegacyExternalStorage).thenReturn(isRequestLegacyExternalStorage)
- whenever(this.adoptPermissions).thenReturn(adoptPermissions)
- whenever(this.implicitPermissions).thenReturn(implicitPermissions)
- whenever(this.requestedPermissions).thenReturn(requestedPermissions)
- whenever(this.permissionGroups).thenReturn(permissionGroups)
- whenever(this.permissions).thenReturn(permissions)
- val signingDetails = mock<SigningDetails> {
- whenever(
- hasCommonSignerWithCapability(any(), any())
- ).thenReturn(isSignatureMatching)
- whenever(hasAncestorOrSelf(any())).thenReturn(isSignatureMatching)
- whenever(
- checkCapability(any<SigningDetails>(), any())
- ).thenReturn(isSignatureMatching)
- }
- whenever(this.signingDetails).thenReturn(signingDetails)
- }
-
- private fun mockParsedPermission(
- permissionName: String,
- packageName: String,
- backgroundPermission: String? = null,
- group: String? = null,
- protectionLevel: Int = PermissionInfo.PROTECTION_NORMAL,
- flags: Int = 0,
- isTree: Boolean = false
- ): ParsedPermission =
- mock {
- whenever(name).thenReturn(permissionName)
- whenever(this.packageName).thenReturn(packageName)
- whenever(metaData).thenReturn(Bundle())
- whenever(this.backgroundPermission).thenReturn(backgroundPermission)
- whenever(this.group).thenReturn(group)
- whenever(this.protectionLevel).thenReturn(protectionLevel)
- whenever(this.flags).thenReturn(flags)
- whenever(this.isTree).thenReturn(isTree)
- }
-
- private fun mockParsedPermissionGroup(
- permissionGroupName: String,
- packageName: String,
- ): ParsedPermissionGroup =
- mock {
- whenever(name).thenReturn(permissionGroupName)
- whenever(this.packageName).thenReturn(packageName)
- whenever(metaData).thenReturn(Bundle())
- }
-
- private fun addPackageState(packageState: PackageState, state: MutableAccessState = oldState) {
- state.mutateExternalState().apply {
- setPackageStates(
- packageStates.toMutableMap().apply {
- put(packageState.packageName, packageState)
- }
- )
- mutateAppIdPackageNames().mutateOrPut(packageState.appId) { MutableIndexedListSet() }
- .add(packageState.packageName)
- }
- }
-
- private fun addDisabledSystemPackageState(
- packageState: PackageState,
- state: MutableAccessState = oldState
- ) = state.mutateExternalState().apply {
- (disabledSystemPackageStates as ArrayMap)[packageState.packageName] = packageState
- }
-
- private fun addPermission(
- parsedPermission: ParsedPermission,
- type: Int = Permission.TYPE_MANIFEST,
- isReconciled: Boolean = true,
- state: MutableAccessState = oldState
- ) {
- val permissionInfo = PackageInfoUtils.generatePermissionInfo(
- parsedPermission,
- PackageManager.GET_META_DATA.toLong()
- )!!
- val appId = state.externalState.packageStates[permissionInfo.packageName]!!.appId
- val permission = Permission(permissionInfo, isReconciled, type, appId)
- if (parsedPermission.isTree) {
- state.mutateSystemState().mutatePermissionTrees()[permission.name] = permission
- } else {
- state.mutateSystemState().mutatePermissions()[permission.name] = permission
- }
- }
-
- private fun addPermissionGroup(
- parsedPermissionGroup: ParsedPermissionGroup,
- state: MutableAccessState = oldState
- ) {
- state.mutateSystemState().mutatePermissionGroups()[parsedPermissionGroup.name] =
- PackageInfoUtils.generatePermissionGroupInfo(
- parsedPermissionGroup,
- PackageManager.GET_META_DATA.toLong()
- )!!
- }
-
- private fun getPermission(
- permissionName: String,
- state: MutableAccessState = newState
- ): Permission? = state.systemState.permissions[permissionName]
-
- private fun getPermissionTree(
- permissionTreeName: String,
- state: MutableAccessState = newState
- ): Permission? = state.systemState.permissionTrees[permissionTreeName]
-
- private fun getPermissionGroup(
- permissionGroupName: String,
- state: MutableAccessState = newState
- ): PermissionGroupInfo? = state.systemState.permissionGroups[permissionGroupName]
-
- private fun getPermissionFlags(
- appId: Int,
- userId: Int,
- permissionName: String,
- state: MutableAccessState = newState
- ): Int =
- state.userStates[userId]?.appIdPermissionFlags?.get(appId).getWithDefault(permissionName, 0)
-
- private fun setPermissionFlags(
- appId: Int,
- userId: Int,
- permissionName: String,
- flags: Int,
- state: MutableAccessState = oldState
- ) =
- state.mutateUserState(userId)!!.mutateAppIdPermissionFlags().mutateOrPut(appId) {
- MutableIndexedMap()
- }.put(permissionName, flags)
-
- companion object {
- private const val PACKAGE_NAME_0 = "packageName0"
- private const val PACKAGE_NAME_1 = "packageName1"
- private const val MISSING_ANDROID_PACKAGE = "missingAndroidPackage"
- private const val PLATFORM_PACKAGE_NAME = "android"
-
- private const val APP_ID_0 = 0
- private const val APP_ID_1 = 1
- private const val PLATFORM_APP_ID = 2
-
- private const val PERMISSION_GROUP_NAME_0 = "permissionGroupName0"
- private const val PERMISSION_GROUP_NAME_1 = "permissionGroupName1"
-
- private const val PERMISSION_TREE_NAME = "permissionTree"
-
- private const val PERMISSION_NAME_0 = "permissionName0"
- private const val PERMISSION_NAME_1 = "permissionName1"
- private const val PERMISSION_NAME_2 = "permissionName2"
- private const val PERMISSION_READ_EXTERNAL_STORAGE =
- Manifest.permission.READ_EXTERNAL_STORAGE
- private const val PERMISSION_POST_NOTIFICATIONS =
- Manifest.permission.POST_NOTIFICATIONS
- private const val PERMISSION_BLUETOOTH_CONNECT =
- Manifest.permission.BLUETOOTH_CONNECT
- private const val PERMISSION_ACCESS_BACKGROUND_LOCATION =
- Manifest.permission.ACCESS_BACKGROUND_LOCATION
- private const val PERMISSION_ACCESS_MEDIA_LOCATION =
- Manifest.permission.ACCESS_MEDIA_LOCATION
-
- private const val USER_ID_0 = 0
- }
-}
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BaseAppIdPermissionPolicyTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BaseAppIdPermissionPolicyTest.kt
new file mode 100644
index 0000000..7966c5c
--- /dev/null
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BaseAppIdPermissionPolicyTest.kt
@@ -0,0 +1,446 @@
+/*
+ * 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.server.permission.test
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.content.pm.PermissionGroupInfo
+import android.content.pm.PermissionInfo
+import android.content.pm.SigningDetails
+import android.os.Build
+import android.os.Bundle
+import android.util.ArrayMap
+import android.util.SparseArray
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.server.extendedtestutils.wheneverStatic
+import com.android.server.permission.access.MutableAccessState
+import com.android.server.permission.access.MutableUserState
+import com.android.server.permission.access.MutateStateScope
+import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports
+import com.android.server.permission.access.permission.AppIdPermissionPolicy
+import com.android.server.permission.access.permission.Permission
+import com.android.server.permission.access.permission.PermissionFlags
+import com.android.server.permission.access.util.hasBits
+import com.android.server.pm.parsing.PackageInfoUtils
+import com.android.server.pm.pkg.AndroidPackage
+import com.android.server.pm.pkg.PackageState
+import com.android.server.pm.pkg.PackageUserState
+import com.android.server.pm.pkg.component.ParsedPermission
+import com.android.server.pm.pkg.component.ParsedPermissionGroup
+import com.android.server.testutils.any
+import com.android.server.testutils.mock
+import com.android.server.testutils.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyLong
+
+/**
+ * Mocking unit test for AppIdPermissionPolicy.
+ */
+@RunWith(AndroidJUnit4::class)
+open class BaseAppIdPermissionPolicyTest {
+ protected lateinit var oldState: MutableAccessState
+ protected lateinit var newState: MutableAccessState
+
+ protected val defaultPermissionGroup = mockParsedPermissionGroup(
+ PERMISSION_GROUP_NAME_0,
+ PACKAGE_NAME_0
+ )
+ protected val defaultPermissionTree = mockParsedPermission(
+ PERMISSION_TREE_NAME,
+ PACKAGE_NAME_0,
+ isTree = true
+ )
+ protected val defaultPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_0)
+
+ protected val appIdPermissionPolicy = AppIdPermissionPolicy()
+
+ @Rule
+ @JvmField
+ val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
+ .spyStatic(PackageInfoUtils::class.java)
+ .build()
+
+ @Before
+ open fun setUp() {
+ oldState = MutableAccessState()
+ createUserState(USER_ID_0)
+ oldState.mutateExternalState().setPackageStates(ArrayMap())
+ oldState.mutateExternalState().setDisabledSystemPackageStates(ArrayMap())
+ mockPackageInfoUtilsGeneratePermissionInfo()
+ mockPackageInfoUtilsGeneratePermissionGroupInfo()
+ }
+
+ protected fun createUserState(userId: Int) {
+ oldState.mutateExternalState().mutateUserIds().add(userId)
+ oldState.mutateUserStatesNoWrite().put(userId, MutableUserState())
+ }
+
+ private fun mockPackageInfoUtilsGeneratePermissionInfo() {
+ wheneverStatic {
+ PackageInfoUtils.generatePermissionInfo(any(ParsedPermission::class.java), anyLong())
+ }.thenAnswer { invocation ->
+ val parsedPermission = invocation.getArgument<ParsedPermission>(0)
+ val generateFlags = invocation.getArgument<Long>(1)
+ PermissionInfo(parsedPermission.backgroundPermission).apply {
+ name = parsedPermission.name
+ packageName = parsedPermission.packageName
+ metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) {
+ parsedPermission.metaData
+ } else {
+ null
+ }
+ @Suppress("DEPRECATION")
+ protectionLevel = parsedPermission.protectionLevel
+ group = parsedPermission.group
+ flags = parsedPermission.flags
+ }
+ }
+ }
+
+ private fun mockPackageInfoUtilsGeneratePermissionGroupInfo() {
+ wheneverStatic {
+ PackageInfoUtils.generatePermissionGroupInfo(
+ any(ParsedPermissionGroup::class.java),
+ anyLong()
+ )
+ }.thenAnswer { invocation ->
+ val parsedPermissionGroup = invocation.getArgument<ParsedPermissionGroup>(0)
+ val generateFlags = invocation.getArgument<Long>(1)
+ @Suppress("DEPRECATION")
+ PermissionGroupInfo().apply {
+ name = parsedPermissionGroup.name
+ packageName = parsedPermissionGroup.packageName
+ metaData = if (generateFlags.toInt().hasBits(PackageManager.GET_META_DATA)) {
+ parsedPermissionGroup.metaData
+ } else {
+ null
+ }
+ flags = parsedPermissionGroup.flags
+ }
+ }
+ }
+
+ @Test
+ fun testOnAppIdRemoved_appIdIsRemoved_permissionFlagsCleared() {
+ val parsedPermission = mockParsedPermission(PERMISSION_NAME_0, PACKAGE_NAME_0)
+ val permissionOwnerPackageState = mockPackageState(
+ APP_ID_0,
+ mockAndroidPackage(PACKAGE_NAME_0, permissions = listOf(parsedPermission))
+ )
+ val requestingPackageState = mockPackageState(
+ APP_ID_1,
+ mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0))
+ )
+ addPackageState(permissionOwnerPackageState)
+ addPackageState(requestingPackageState)
+ addPermission(parsedPermission)
+ setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, PermissionFlags.INSTALL_GRANTED)
+
+ mutateState {
+ with(appIdPermissionPolicy) {
+ onAppIdRemoved(APP_ID_1)
+ }
+ }
+
+ val actualFlags = getPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After onAppIdRemoved() is called for appId $APP_ID_1 that requests a permission" +
+ " owns by appId $APP_ID_0 with existing permission flags. The actual permission" +
+ " flags $actualFlags should be null"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ @Test
+ fun testOnPackageRemoved_packageIsRemoved_permissionsAreTrimmedAndStatesAreEvaluated() {
+ // TODO
+ // shouldn't reuse test cases because it's really different despite it's also for
+ // trim permission states. It's different because it's package removal
+ }
+
+ @Test
+ fun testOnPackageInstalled_nonSystemAppIsInstalled_upgradeExemptFlagIsCleared() {
+ // TODO
+ // should be fine for it to be its own test cases and not to re-use
+ // clearRestrictedPermissionImplicitExemption
+ }
+
+ @Test
+ fun testOnPackageInstalled_systemAppIsInstalled_upgradeExemptFlagIsRetained() {
+ // TODO
+ }
+
+ @Test
+ fun testOnPackageInstalled_requestedPermissionAlsoRequestedBySystemApp_exemptFlagIsRetained() {
+ // TODO
+ }
+
+ @Test
+ fun testOnPackageInstalled_restrictedPermissionsNotExempt_getsRestrictionFlags() {
+ // TODO
+ }
+
+ @Test
+ fun testOnPackageInstalled_restrictedPermissionsIsExempted_clearsRestrictionFlags() {
+ // TODO
+ }
+
+ @Test
+ fun testOnStateMutated_notEmpty_isCalledForEachListener() {
+ // TODO
+ }
+
+ /**
+ * Mock an AndroidPackage with PACKAGE_NAME_0, PERMISSION_NAME_0 and PERMISSION_GROUP_NAME_0
+ */
+ protected fun mockSimpleAndroidPackage(): AndroidPackage =
+ mockAndroidPackage(
+ PACKAGE_NAME_0,
+ permissionGroups = listOf(defaultPermissionGroup),
+ permissions = listOf(defaultPermissionTree, defaultPermission)
+ )
+
+ protected inline fun mutateState(action: MutateStateScope.() -> Unit) {
+ newState = oldState.toMutable()
+ MutateStateScope(oldState, newState).action()
+ }
+
+ protected fun mockPackageState(
+ appId: Int,
+ packageName: String,
+ isSystem: Boolean = false,
+ ): PackageState =
+ mock {
+ whenever(this.appId).thenReturn(appId)
+ whenever(this.packageName).thenReturn(packageName)
+ whenever(androidPackage).thenReturn(null)
+ whenever(this.isSystem).thenReturn(isSystem)
+ }
+
+ protected fun mockPackageState(
+ appId: Int,
+ androidPackage: AndroidPackage,
+ isSystem: Boolean = false,
+ isPrivileged: Boolean = false,
+ isProduct: Boolean = false,
+ isInstantApp: Boolean = false,
+ isVendor: Boolean = false
+ ): PackageState =
+ mock {
+ whenever(this.appId).thenReturn(appId)
+ whenever(this.androidPackage).thenReturn(androidPackage)
+ val packageName = androidPackage.packageName
+ whenever(this.packageName).thenReturn(packageName)
+ whenever(this.isSystem).thenReturn(isSystem)
+ whenever(this.isPrivileged).thenReturn(isPrivileged)
+ whenever(this.isProduct).thenReturn(isProduct)
+ whenever(this.isVendor).thenReturn(isVendor)
+ val userStates = SparseArray<PackageUserState>().apply {
+ put(USER_ID_0, mock { whenever(this.isInstantApp).thenReturn(isInstantApp) })
+ }
+ whenever(this.userStates).thenReturn(userStates)
+ }
+
+ protected fun mockAndroidPackage(
+ packageName: String,
+ targetSdkVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE,
+ isRequestLegacyExternalStorage: Boolean = false,
+ adoptPermissions: List<String> = emptyList(),
+ implicitPermissions: Set<String> = emptySet(),
+ requestedPermissions: Set<String> = emptySet(),
+ permissionGroups: List<ParsedPermissionGroup> = emptyList(),
+ permissions: List<ParsedPermission> = emptyList(),
+ isSignatureMatching: Boolean = false
+ ): AndroidPackage =
+ mock {
+ whenever(this.packageName).thenReturn(packageName)
+ whenever(this.targetSdkVersion).thenReturn(targetSdkVersion)
+ whenever(this.isRequestLegacyExternalStorage).thenReturn(isRequestLegacyExternalStorage)
+ whenever(this.adoptPermissions).thenReturn(adoptPermissions)
+ whenever(this.implicitPermissions).thenReturn(implicitPermissions)
+ whenever(this.requestedPermissions).thenReturn(requestedPermissions)
+ whenever(this.permissionGroups).thenReturn(permissionGroups)
+ whenever(this.permissions).thenReturn(permissions)
+ val signingDetails = mock<SigningDetails> {
+ whenever(
+ hasCommonSignerWithCapability(any(), any())
+ ).thenReturn(isSignatureMatching)
+ whenever(hasAncestorOrSelf(any())).thenReturn(isSignatureMatching)
+ whenever(
+ checkCapability(any<SigningDetails>(), any())
+ ).thenReturn(isSignatureMatching)
+ }
+ whenever(this.signingDetails).thenReturn(signingDetails)
+ }
+
+ protected fun mockParsedPermission(
+ permissionName: String,
+ packageName: String,
+ backgroundPermission: String? = null,
+ group: String? = null,
+ protectionLevel: Int = PermissionInfo.PROTECTION_NORMAL,
+ flags: Int = 0,
+ isTree: Boolean = false
+ ): ParsedPermission =
+ mock {
+ whenever(name).thenReturn(permissionName)
+ whenever(this.packageName).thenReturn(packageName)
+ whenever(metaData).thenReturn(Bundle())
+ whenever(this.backgroundPermission).thenReturn(backgroundPermission)
+ whenever(this.group).thenReturn(group)
+ whenever(this.protectionLevel).thenReturn(protectionLevel)
+ whenever(this.flags).thenReturn(flags)
+ whenever(this.isTree).thenReturn(isTree)
+ }
+
+ protected fun mockParsedPermissionGroup(
+ permissionGroupName: String,
+ packageName: String,
+ ): ParsedPermissionGroup =
+ mock {
+ whenever(name).thenReturn(permissionGroupName)
+ whenever(this.packageName).thenReturn(packageName)
+ whenever(metaData).thenReturn(Bundle())
+ }
+
+ protected fun addPackageState(
+ packageState: PackageState,
+ state: MutableAccessState = oldState
+ ) {
+ state.mutateExternalState().apply {
+ setPackageStates(
+ packageStates.toMutableMap().apply {
+ put(packageState.packageName, packageState)
+ }
+ )
+ mutateAppIdPackageNames().mutateOrPut(packageState.appId) { MutableIndexedListSet() }
+ .add(packageState.packageName)
+ }
+ }
+
+ protected fun addDisabledSystemPackageState(
+ packageState: PackageState,
+ state: MutableAccessState = oldState
+ ) = state.mutateExternalState().apply {
+ (disabledSystemPackageStates as ArrayMap)[packageState.packageName] = packageState
+ }
+
+ protected fun addPermission(
+ parsedPermission: ParsedPermission,
+ type: Int = Permission.TYPE_MANIFEST,
+ isReconciled: Boolean = true,
+ state: MutableAccessState = oldState
+ ) {
+ val permissionInfo = PackageInfoUtils.generatePermissionInfo(
+ parsedPermission,
+ PackageManager.GET_META_DATA.toLong()
+ )!!
+ val appId = state.externalState.packageStates[permissionInfo.packageName]!!.appId
+ val permission = Permission(permissionInfo, isReconciled, type, appId)
+ if (parsedPermission.isTree) {
+ state.mutateSystemState().mutatePermissionTrees()[permission.name] = permission
+ } else {
+ state.mutateSystemState().mutatePermissions()[permission.name] = permission
+ }
+ }
+
+ protected fun addPermissionGroup(
+ parsedPermissionGroup: ParsedPermissionGroup,
+ state: MutableAccessState = oldState
+ ) {
+ state.mutateSystemState().mutatePermissionGroups()[parsedPermissionGroup.name] =
+ PackageInfoUtils.generatePermissionGroupInfo(
+ parsedPermissionGroup,
+ PackageManager.GET_META_DATA.toLong()
+ )!!
+ }
+
+ protected fun getPermission(
+ permissionName: String,
+ state: MutableAccessState = newState
+ ): Permission? = state.systemState.permissions[permissionName]
+
+ protected fun getPermissionTree(
+ permissionTreeName: String,
+ state: MutableAccessState = newState
+ ): Permission? = state.systemState.permissionTrees[permissionTreeName]
+
+ protected fun getPermissionGroup(
+ permissionGroupName: String,
+ state: MutableAccessState = newState
+ ): PermissionGroupInfo? = state.systemState.permissionGroups[permissionGroupName]
+
+ protected fun getPermissionFlags(
+ appId: Int,
+ userId: Int,
+ permissionName: String,
+ state: MutableAccessState = newState
+ ): Int =
+ state.userStates[userId]?.appIdPermissionFlags?.get(appId).getWithDefault(permissionName, 0)
+
+ protected fun setPermissionFlags(
+ appId: Int,
+ userId: Int,
+ permissionName: String,
+ flags: Int,
+ state: MutableAccessState = oldState
+ ) =
+ state.mutateUserState(userId)!!.mutateAppIdPermissionFlags().mutateOrPut(appId) {
+ MutableIndexedMap()
+ }.put(permissionName, flags)
+
+ companion object {
+ @JvmStatic protected val PACKAGE_NAME_0 = "packageName0"
+ @JvmStatic protected val PACKAGE_NAME_1 = "packageName1"
+ @JvmStatic protected val PACKAGE_NAME_2 = "packageName2"
+ @JvmStatic protected val MISSING_ANDROID_PACKAGE = "missingAndroidPackage"
+ @JvmStatic protected val PLATFORM_PACKAGE_NAME = "android"
+
+ @JvmStatic protected val APP_ID_0 = 0
+ @JvmStatic protected val APP_ID_1 = 1
+ @JvmStatic protected val PLATFORM_APP_ID = 2
+
+ @JvmStatic protected val PERMISSION_GROUP_NAME_0 = "permissionGroupName0"
+ @JvmStatic protected val PERMISSION_GROUP_NAME_1 = "permissionGroupName1"
+
+ @JvmStatic protected val PERMISSION_TREE_NAME = "permissionTree"
+
+ @JvmStatic protected val PERMISSION_NAME_0 = "permissionName0"
+ @JvmStatic protected val PERMISSION_NAME_1 = "permissionName1"
+ @JvmStatic protected val PERMISSION_NAME_2 = "permissionName2"
+ @JvmStatic protected val PERMISSION_READ_EXTERNAL_STORAGE =
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ @JvmStatic protected val PERMISSION_POST_NOTIFICATIONS =
+ Manifest.permission.POST_NOTIFICATIONS
+ @JvmStatic protected val PERMISSION_BLUETOOTH_CONNECT =
+ Manifest.permission.BLUETOOTH_CONNECT
+ @JvmStatic protected val PERMISSION_ACCESS_BACKGROUND_LOCATION =
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
+ @JvmStatic protected val PERMISSION_ACCESS_MEDIA_LOCATION =
+ Manifest.permission.ACCESS_MEDIA_LOCATION
+
+ @JvmStatic protected val USER_ID_0 = 0
+ @JvmStatic protected val USER_ID_NEW = 1
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 349a597..430f600 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -687,7 +687,7 @@
swipeAndHold(initCoords, edgeCoords);
assertTrue(mMgh.mCurrentState == mMgh.mSinglePanningState);
- assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_LEFT_EDGE);
+ assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_LEFT_EDGE);
assertTrue(isZoomed());
}
@@ -711,7 +711,7 @@
swipeAndHold(initCoords, edgeCoords);
assertTrue(mMgh.mCurrentState == mMgh.mSinglePanningState);
- assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_RIGHT_EDGE);
+ assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_RIGHT_EDGE);
assertTrue(isZoomed());
}
@@ -734,7 +734,7 @@
swipeAndHold(initCoords, edgeCoords);
- assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_VERTICAL_EDGE);
+ assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_VERTICAL_EDGE);
assertTrue(isZoomed());
}
@@ -756,7 +756,7 @@
swipeAndHold(initCoords, edgeCoords);
- assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_NONE);
+ assertTrue(mMgh.mOverscrollHandler.mOverscrollState == mMgh.OVERSCROLL_NONE);
assertTrue(isZoomed());
}