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