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