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 android.util.Log 26 import androidx.core.content.getSystemService 27 import androidx.test.platform.app.InstrumentationRegistry 28 import java.util.concurrent.CompletableFuture 29 import java.util.concurrent.TimeUnit 30 import org.junit.Assume.assumeTrue 31 import org.junit.AssumptionViolatedException 32 33 /** A helper class to manage [RoleManager.ROLE_NOTES] in end to end tests. */ 34 class NotesRoleUtil(private val context: Context) { 35 36 private val roleManager = context.getSystemService<RoleManager>()!! 37 38 /** 39 * Checks if the following assumptions are true: 40 * - Android version is at least the supplied required android version. By default U+. 41 * - Notes role is available. 42 * - The required package is installed. 43 * 44 * @throws [AssumptionViolatedException] when any of the above assumptions are not met to help 45 * skip a test 46 */ checkAndroidRolePackageAssumptionsnull47 fun checkAndroidRolePackageAssumptions( 48 requiredAndroidVersion: Int = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, 49 requiredPackage: String, 50 ) { 51 assumeTrue( 52 "Build SDK should be at least $requiredAndroidVersion", 53 Build.VERSION.SDK_INT >= requiredAndroidVersion 54 ) 55 assumeTrue( 56 "Notes role should be enabled", 57 roleManager.isRoleAvailable(RoleManager.ROLE_NOTES) 58 ) 59 assumeTrue("$requiredPackage should be installed", isPackageInstalled(requiredPackage)) 60 } 61 62 /** Returns the Notes role holder package name. */ getRoleHolderPackageNamenull63 fun getRoleHolderPackageName(userHandle: UserHandle = context.user): String = 64 callWithManageRoleHolderPermission { 65 roleManager 66 .getRoleHoldersAsUser(RoleManager.ROLE_NOTES, userHandle) 67 .firstOrNull() 68 .orEmpty() 69 } 70 71 /** Force stops the supplied package */ forceStopPackagenull72 fun forceStopPackage(packageName: String) { 73 DeviceHelpers.shell("am force-stop $packageName") 74 } 75 76 /** 77 * Sets the Notes role holder to "None" by clearing existing role holders. This is possible 78 * because Notes role is an exclusive role. 79 */ clearRoleHoldernull80 fun clearRoleHolder(userHandle: UserHandle = context.user) { 81 val clearRoleHoldersFuture = CompletableFuture<Boolean>() 82 callWithManageRoleHolderPermission { 83 roleManager.clearRoleHoldersAsUser( 84 RoleManager.ROLE_NOTES, 85 /* flags= */ 0, 86 userHandle, 87 context.mainExecutor, 88 clearRoleHoldersFuture::complete 89 ) 90 } 91 92 // Synchronously wait for the role to update. 93 assert( 94 clearRoleHoldersFuture.get( 95 ROLE_MANAGER_VERIFICATION_TIMEOUT_IN_SECONDS, 96 TimeUnit.SECONDS 97 ) 98 ) { 99 "Failed to clear notes role holder" 100 } 101 } 102 103 /** Sets the supplied package as the Notes role holder app. */ setRoleHoldernull104 fun setRoleHolder(packageName: String, userHandle: UserHandle = context.user) { 105 val currentRoleHolderPackageName = getRoleHolderPackageName() 106 Log.d( 107 TAG, 108 "Trying to set note role to $packageName. Current role holder: " + 109 "$currentRoleHolderPackageName" 110 ) 111 112 // Return early if current role holder package is the same as supplied package name. 113 if (currentRoleHolderPackageName == packageName) { 114 return 115 } 116 117 // Notes role is an exclusive role, so clear other role holders before adding the supplied 118 // package to the role. RoleManager has an "addRoleHolder" but no setRoleHolder API so we 119 // have to clear the current role holder before adding the supplied package as role holder. 120 clearRoleHolder(userHandle) 121 Log.d(TAG, "After clear note role") 122 123 // If the supplied package name is empty it indicates that we want to select "None" as the 124 // new Notes role holder, so return early in this case. 125 if (packageName.isEmpty()) { 126 return 127 } 128 129 // Add the supplied package name to the Notes role. 130 val addRoleHolderFuture = CompletableFuture<Boolean>() 131 Log.d(TAG, "Start setting note holder to $packageName") 132 callWithManageRoleHolderPermission { 133 roleManager.addRoleHolderAsUser( 134 RoleManager.ROLE_NOTES, 135 packageName, 136 /* flags= */ 0, 137 userHandle, 138 context.mainExecutor, 139 ) { 140 Log.d(TAG, "Callback for setting note role to $packageName: $it") 141 addRoleHolderFuture.complete(it) 142 } 143 } 144 145 // Synchronously wait for the role to update. 146 assert( 147 addRoleHolderFuture.get(ROLE_MANAGER_VERIFICATION_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS) 148 ) { 149 "Failed to set $packageName as default notes role holder" 150 } 151 Log.d(TAG, "After setting note role to $packageName") 152 } 153 154 /** Calls the supplied callable with the provided permissions using shell identity. */ callWithShellIdentityPermissionsnull155 fun <T> callWithShellIdentityPermissions(vararg permissions: String, callable: () -> T): T { 156 val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation 157 158 try { 159 uiAutomation.adoptShellPermissionIdentity(*permissions) 160 return callable() 161 } finally { 162 uiAutomation.dropShellPermissionIdentity() 163 } 164 } 165 isPackageInstallednull166 private fun isPackageInstalled(packageName: String) = 167 runCatching { context.packageManager.getPackageInfo(packageName, /* flags= */ 0) }.isSuccess 168 callWithManageRoleHolderPermissionnull169 private fun <T> callWithManageRoleHolderPermission(callable: () -> T): T { 170 return callWithShellIdentityPermissions(MANAGE_ROLE_HOLDERS, INTERACT_ACROSS_USERS) { 171 callable() 172 } 173 } 174 175 private companion object { 176 const val TAG = "NotesRoleUtil" 177 const val ROLE_MANAGER_VERIFICATION_TIMEOUT_IN_SECONDS = 60L 178 } 179 } 180