1 // 2 // Copyright (C) 2023 The Android Open Source Project 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 // 16 package android.platform.helpers.notesrole 17 18 import android.Manifest.permission.INTERACT_ACROSS_USERS 19 import android.Manifest.permission.MANAGE_ROLE_HOLDERS 20 import android.app.role.RoleManager 21 import android.content.Context 22 import android.os.Build 23 import android.os.UserHandle 24 import android.platform.uiautomator_helpers.DeviceHelpers 25 import androidx.core.content.getSystemService 26 import androidx.test.platform.app.InstrumentationRegistry 27 import java.util.concurrent.CompletableFuture 28 import java.util.concurrent.TimeUnit 29 import org.junit.Assume.assumeTrue 30 import org.junit.AssumptionViolatedException 31 32 /** A helper class to manage [RoleManager.ROLE_NOTES] in end to end tests. */ 33 class NotesRoleUtil(private val context: Context) { 34 35 private val roleManager = context.getSystemService<RoleManager>()!! 36 37 /** 38 * Checks if the following assumptions are true: 39 * - Android version is at least the supplied required android version. By default U+. 40 * - Notes role is available. 41 * - The required package is installed. 42 * 43 * @throws [AssumptionViolatedException] when any of the above assumptions are not met to help 44 * skip a test 45 */ checkAndroidRolePackageAssumptionsnull46 fun checkAndroidRolePackageAssumptions( 47 requiredAndroidVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, 48 requiredPackage: String, 49 ) { 50 assumeTrue( 51 "Build SDK should be at least $requiredAndroidVersion", 52 Build.VERSION.SDK_INT >= requiredAndroidVersion 53 ) 54 assumeTrue( 55 "Notes role should be enabled", 56 roleManager.isRoleAvailable(RoleManager.ROLE_NOTES) 57 ) 58 assumeTrue("$requiredPackage should be installed", isPackageInstalled(requiredPackage)) 59 } 60 61 /** Returns the Notes role holder package name. */ getRoleHolderPackageNamenull62 fun getRoleHolderPackageName(userHandle: UserHandle = context.user): String = 63 callWithManageRoleHolderPermission { 64 roleManager 65 .getRoleHoldersAsUser(RoleManager.ROLE_NOTES, userHandle) 66 .firstOrNull() 67 .orEmpty() 68 } 69 70 /** Force stops the supplied package */ forceStopPackagenull71 fun forceStopPackage(packageName: String) { 72 DeviceHelpers.shell("am force-stop $packageName") 73 } 74 75 /** 76 * Sets the Notes role holder to "None" by clearing existing role holders. This is possible 77 * because Notes role is an exclusive role. 78 */ clearRoleHoldernull79 fun clearRoleHolder(userHandle: UserHandle = context.user) { 80 val clearRoleHoldersFuture = CompletableFuture<Boolean>() 81 callWithManageRoleHolderPermission { 82 roleManager.clearRoleHoldersAsUser( 83 RoleManager.ROLE_NOTES, 84 /* flags= */ 0, 85 userHandle, 86 context.mainExecutor, 87 clearRoleHoldersFuture::complete 88 ) 89 } 90 91 // Synchronously wait for the role to update. 92 assert( 93 clearRoleHoldersFuture.get( 94 ROLE_MANAGER_VERIFICATION_TIMEOUT_IN_SECONDS, 95 TimeUnit.SECONDS 96 ) 97 ) { 98 "Failed to clear notes role holder" 99 } 100 } 101 102 /** Sets the supplied package as the Notes role holder app. */ setRoleHoldernull103 fun setRoleHolder(packageName: String, userHandle: UserHandle = context.user) { 104 val currentRoleHolderPackageName = getRoleHolderPackageName() 105 106 // Return early if current role holder package is the same as supplied package name. 107 if (currentRoleHolderPackageName == packageName) { 108 return 109 } 110 111 // Notes role is an exclusive role, so clear other role holders before adding the supplied 112 // package to the role. RoleManager has an "addRoleHolder" but no setRoleHolder API so we 113 // have to clear the current role holder before adding the supplied package as role holder. 114 clearRoleHolder(userHandle) 115 116 // If the supplied package name is empty it indicates that we want to select "None" as the 117 // new Notes role holder, so return early in this case. 118 if (packageName.isEmpty()) { 119 return 120 } 121 122 // Add the supplied package name to the Notes role. 123 val addRoleHolderFuture = CompletableFuture<Boolean>() 124 callWithManageRoleHolderPermission { 125 roleManager.addRoleHolderAsUser( 126 RoleManager.ROLE_NOTES, 127 packageName, 128 /* flags= */ 0, 129 userHandle, 130 context.mainExecutor, 131 addRoleHolderFuture::complete 132 ) 133 } 134 135 // Synchronously wait for the role to update. 136 assert( 137 addRoleHolderFuture.get(ROLE_MANAGER_VERIFICATION_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) 138 ) { 139 "Failed to set $packageName as default notes role holder" 140 } 141 } 142 143 /** Calls the supplied callable with the provided permissions using shell identity. */ callWithShellIdentityPermissionsnull144 fun <T> callWithShellIdentityPermissions(vararg permissions: String, callable: () -> T): T { 145 val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation 146 147 try { 148 uiAutomation.adoptShellPermissionIdentity(*permissions) 149 return callable() 150 } finally { 151 uiAutomation.dropShellPermissionIdentity() 152 } 153 } 154 isPackageInstallednull155 private fun isPackageInstalled(packageName: String) = 156 runCatching { context.packageManager.getPackageInfo(packageName, /* flags= */ 0) }.isSuccess 157 callWithManageRoleHolderPermissionnull158 private fun <T> callWithManageRoleHolderPermission(callable: () -> T): T { 159 return callWithShellIdentityPermissions(MANAGE_ROLE_HOLDERS, INTERACT_ACROSS_USERS) { 160 callable() 161 } 162 } 163 164 private companion object { 165 const val ROLE_MANAGER_VERIFICATION_TIMEOUT_IN_SECONDS = 10L 166 } 167 } 168