1 /* 2 * Copyright (C) 2024 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 17 package android.healthconnect.cts.utils; 18 19 import static android.content.pm.PackageManager.GET_PERMISSIONS; 20 import static android.healthconnect.cts.utils.TestUtils.getHealthConnectManager; 21 22 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 23 24 import static com.google.common.base.Preconditions.checkArgument; 25 26 import android.app.UiAutomation; 27 import android.content.Context; 28 import android.content.pm.PackageInfo; 29 import android.content.pm.PackageManager; 30 import android.health.connect.HealthConnectManager; 31 import android.health.connect.HealthPermissions; 32 import android.os.UserHandle; 33 34 import androidx.annotation.Nullable; 35 36 import com.android.compatibility.common.util.SystemUtil; 37 import com.android.compatibility.common.util.ThrowingRunnable; 38 import com.android.compatibility.common.util.ThrowingSupplier; 39 40 import com.google.common.collect.Sets; 41 42 import java.time.Instant; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.Collection; 46 import java.util.List; 47 import java.util.Set; 48 49 public final class PermissionHelper { 50 51 public static final String MANAGE_HEALTH_DATA = HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION; 52 public static final String READ_EXERCISE_ROUTE_PERMISSION = 53 "android.permission.health.READ_EXERCISE_ROUTE"; 54 55 public static final String READ_EXERCISE_ROUTES = 56 "android.permission.health.READ_EXERCISE_ROUTES"; 57 private static final String MANAGE_HEALTH_PERMISSIONS = 58 HealthPermissions.MANAGE_HEALTH_PERMISSIONS; 59 private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health."; 60 61 /** Returns permissions declared in the Manifest of the given package. */ getDeclaredHealthPermissions(String pkgName)62 public static List<String> getDeclaredHealthPermissions(String pkgName) { 63 final PackageInfo pi = getAppPackageInfo(pkgName); 64 final String[] requestedPermissions = pi.requestedPermissions; 65 66 if (requestedPermissions == null) { 67 return List.of(); 68 } 69 70 return Arrays.stream(requestedPermissions) 71 .filter(permission -> permission.startsWith(HEALTH_PERMISSION_PREFIX)) 72 .toList(); 73 } 74 getGrantedHealthPermissions(String pkgName)75 public static List<String> getGrantedHealthPermissions(String pkgName) { 76 final PackageInfo pi = getAppPackageInfo(pkgName); 77 final String[] requestedPermissions = pi.requestedPermissions; 78 final int[] requestedPermissionsFlags = pi.requestedPermissionsFlags; 79 80 if (requestedPermissions == null) { 81 return List.of(); 82 } 83 84 final List<String> permissions = new ArrayList<>(); 85 86 for (int i = 0; i < requestedPermissions.length; i++) { 87 if ((requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) { 88 if (requestedPermissions[i].startsWith(HEALTH_PERMISSION_PREFIX)) { 89 permissions.add(requestedPermissions[i]); 90 } 91 } 92 } 93 94 return permissions; 95 } 96 getAppPackageInfo(String pkgName)97 private static PackageInfo getAppPackageInfo(String pkgName) { 98 final Context targetContext = androidx.test.InstrumentationRegistry.getTargetContext(); 99 return runWithShellPermissionIdentity( 100 () -> 101 targetContext 102 .getPackageManager() 103 .getPackageInfo( 104 pkgName, 105 PackageManager.PackageInfoFlags.of(GET_PERMISSIONS))); 106 } 107 grantPermission(String pkgName, String permission)108 public static void grantPermission(String pkgName, String permission) { 109 HealthConnectManager service = getHealthConnectManager(); 110 runWithShellPermissionIdentity( 111 () -> 112 service.getClass() 113 .getMethod("grantHealthPermission", String.class, String.class) 114 .invoke(service, pkgName, permission), 115 MANAGE_HEALTH_PERMISSIONS); 116 } 117 118 /** Grants {@code permissions} to the app with {@code pkgName}. */ grantPermissions(String pkgName, Collection<String> permissions)119 public static void grantPermissions(String pkgName, Collection<String> permissions) { 120 for (String permission : permissions) { 121 grantPermission(pkgName, permission); 122 } 123 } 124 revokePermission(String pkgName, String permission)125 public static void revokePermission(String pkgName, String permission) { 126 HealthConnectManager service = getHealthConnectManager(); 127 runWithShellPermissionIdentity( 128 () -> 129 service.getClass() 130 .getMethod( 131 "revokeHealthPermission", 132 String.class, 133 String.class, 134 String.class) 135 .invoke(service, pkgName, permission, null), 136 MANAGE_HEALTH_PERMISSIONS); 137 } 138 139 /** 140 * Utility method to call {@link HealthConnectManager#revokeAllHealthPermissions(String, 141 * String)}. 142 */ revokeAllPermissions(String packageName, @Nullable String reason)143 public static void revokeAllPermissions(String packageName, @Nullable String reason) { 144 HealthConnectManager service = getHealthConnectManager(); 145 runWithShellPermissionIdentity( 146 () -> 147 service.getClass() 148 .getMethod("revokeAllHealthPermissions", String.class, String.class) 149 .invoke(service, packageName, reason), 150 MANAGE_HEALTH_PERMISSIONS); 151 } 152 153 /** 154 * Same as {@link #revokeAllPermissions(String, String)} but with a delay to wait for grant time 155 * to be updated. 156 */ revokeAllPermissionsWithDelay(String packageName, @Nullable String reason)157 public static void revokeAllPermissionsWithDelay(String packageName, @Nullable String reason) 158 throws InterruptedException { 159 revokeAllPermissions(packageName, reason); 160 Thread.sleep(500); 161 } 162 163 /** Revokes all granted Health permissions and re-grants them back. */ revokeAndThenGrantHealthPermissions(String packageName)164 public static void revokeAndThenGrantHealthPermissions(String packageName) { 165 List<String> healthPerms = getGrantedHealthPermissions(packageName); 166 167 revokeHealthPermissions(packageName); 168 169 for (String perm : healthPerms) { 170 grantPermission(packageName, perm); 171 } 172 } 173 revokeHealthPermissions(String packageName)174 public static void revokeHealthPermissions(String packageName) { 175 runWithShellPermissionIdentity(() -> revokeHealthPermissionsPrivileged(packageName)); 176 } 177 revokeHealthPermissionsPrivileged(String packageName)178 private static void revokeHealthPermissionsPrivileged(String packageName) 179 throws PackageManager.NameNotFoundException { 180 final Context targetContext = androidx.test.InstrumentationRegistry.getTargetContext(); 181 final PackageManager packageManager = targetContext.getPackageManager(); 182 final UserHandle user = targetContext.getUser(); 183 184 final PackageInfo packageInfo = 185 packageManager.getPackageInfo( 186 packageName, 187 PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS)); 188 189 final String[] permissions = packageInfo.requestedPermissions; 190 if (permissions == null) { 191 return; 192 } 193 194 for (String permission : permissions) { 195 if (permission.startsWith(HEALTH_PERMISSION_PREFIX)) { 196 packageManager.revokeRuntimePermission(packageName, permission, user); 197 } 198 } 199 } 200 201 /** 202 * Utility method to call {@link 203 * HealthConnectManager#getHealthDataHistoricalAccessStartDate(String)}. 204 */ getHealthDataHistoricalAccessStartDate(String packageName)205 public static Instant getHealthDataHistoricalAccessStartDate(String packageName) { 206 HealthConnectManager service = getHealthConnectManager(); 207 return (Instant) 208 runWithShellPermissionIdentity( 209 () -> 210 service.getClass() 211 .getMethod( 212 "getHealthDataHistoricalAccessStartDate", 213 String.class) 214 .invoke(service, packageName), 215 MANAGE_HEALTH_PERMISSIONS); 216 } 217 218 /** Revokes permission for the package for the duration of the runnable. */ runWithRevokedPermissions( String packageName, String permission, ThrowingRunnable runnable)219 public static void runWithRevokedPermissions( 220 String packageName, String permission, ThrowingRunnable runnable) throws Exception { 221 runWithRevokedPermissions( 222 (ThrowingSupplier<Void>) 223 () -> { 224 runnable.run(); 225 return null; 226 }, 227 packageName, 228 permission); 229 } 230 231 /** Revokes permission for the package for the duration of the supplier. */ runWithRevokedPermission( String packageName, String permission, ThrowingSupplier<T> supplier)232 public static <T> T runWithRevokedPermission( 233 String packageName, String permission, ThrowingSupplier<T> supplier) throws Exception { 234 return runWithRevokedPermissions(supplier, packageName, permission); 235 } 236 237 /** Revokes permission for the package for the duration of the supplier. */ runWithRevokedPermissions( ThrowingSupplier<T> supplier, String packageName, String... permissions)238 public static <T> T runWithRevokedPermissions( 239 ThrowingSupplier<T> supplier, String packageName, String... permissions) 240 throws Exception { 241 Context context = androidx.test.InstrumentationRegistry.getTargetContext(); 242 checkArgument( 243 !context.getPackageName().equals(packageName), 244 "Can not be called on self, only on other apps"); 245 246 UiAutomation uiAutomation = 247 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() 248 .getUiAutomation(); 249 250 var grantedPermissions = 251 Sets.intersection( 252 Set.copyOf(getGrantedHealthPermissions(packageName)), Set.of(permissions)); 253 254 try { 255 grantedPermissions.forEach( 256 permission -> uiAutomation.revokeRuntimePermission(packageName, permission)); 257 return supplier.get(); 258 } finally { 259 grantedPermissions.forEach( 260 permission -> uiAutomation.grantRuntimePermission(packageName, permission)); 261 } 262 } 263 264 /** Flags the permission as USER_FIXED for the duration of the supplier. */ runWithUserFixedPermission( String packageName, String permission, ThrowingSupplier<T> supplier)265 public static <T> T runWithUserFixedPermission( 266 String packageName, String permission, ThrowingSupplier<T> supplier) throws Exception { 267 SystemUtil.runShellCommand( 268 String.format("pm set-permission-flags %s %s user-fixed", packageName, permission)); 269 try { 270 return supplier.get(); 271 } finally { 272 SystemUtil.runShellCommand( 273 String.format( 274 "pm clear-permission-flags %s %s user-fixed", packageName, permission)); 275 } 276 } 277 278 /** 279 * Sets the device config value for the duration of the supplier. 280 * 281 * <p>Kills the HC controller after each device config update as the most reliable way of making 282 * sure the controller picks up the updated value. Otherwise the callback which the controller 283 * uses to listen to device config changes might arrive late (and usually does). 284 */ runWithDeviceConfigForController( String key, String value, ThrowingSupplier<T> supplier)285 public static <T> T runWithDeviceConfigForController( 286 String key, String value, ThrowingSupplier<T> supplier) throws Exception { 287 DeviceConfigRule rule = new DeviceConfigRule(key, value); 288 try { 289 rule.before(); 290 killHealthConnectController(); 291 return supplier.get(); 292 } catch (Throwable e) { 293 throw new Exception(e); 294 } finally { 295 rule.after(); 296 killHealthConnectController(); 297 } 298 } 299 300 /** Kills Health Connect controller. */ killHealthConnectController()301 private static void killHealthConnectController() { 302 SystemUtil.runShellCommandOrThrow( 303 "am force-stop com.google.android.healthconnect.controller"); 304 } 305 } 306