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.uiautomatorhelpers.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