1 /*
2  * Copyright (C) 2022 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.car.test;
18 
19 import static java.lang.annotation.ElementType.METHOD;
20 import static java.lang.annotation.ElementType.TYPE;
21 import static java.lang.annotation.RetentionPolicy.RUNTIME;
22 
23 import android.app.UiAutomation;
24 import android.util.ArraySet;
25 import android.util.Log;
26 
27 import androidx.test.platform.app.InstrumentationRegistry;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import org.junit.rules.TestRule;
32 import org.junit.runner.Description;
33 import org.junit.runners.model.Statement;
34 
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.Target;
37 import java.util.Set;
38 
39 /**
40  * {@code JUnit} rule that uses {@link UiAutomation} to adopt the Shell permissions defined by
41  * {@link EnsureHasPermission}.
42  */
43 // TODO(b/250108245): move to Bedstead itself or merge with
44 // {@code com.android.compatibility.common.util.AdoptShellPermissionsRule} (which currently takes
45 // the permissions from the constructor)
46 public final class PermissionsCheckerRule implements TestRule {
47 
48     @VisibleForTesting
49     static final String TAG = PermissionsCheckerRule.class.getSimpleName();
50 
51     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
52 
53     private final UiAutomation mUiAutomation;
54 
PermissionsCheckerRule()55     public PermissionsCheckerRule() {
56         this(InstrumentationRegistry.getInstrumentation().getUiAutomation());
57     }
58 
59     @VisibleForTesting
PermissionsCheckerRule(UiAutomation uiAutomation)60     PermissionsCheckerRule(UiAutomation uiAutomation) {
61         mUiAutomation = uiAutomation;
62     }
63 
64     @Override
apply(Statement base, Description description)65     public Statement apply(Statement base, Description description) {
66         return new Statement() {
67             @Override
68             public void evaluate() throws Throwable {
69                 if (DBG) {
70                     Log.d(TAG, "evaluating " + description.getDisplayName());
71                 }
72 
73                 Set<String> permissionsBefore = mUiAutomation.getAdoptedShellPermissions();
74                 if (permissionsBefore != null && !permissionsBefore.isEmpty()) {
75                     Log.w(TAG, "Permissions were adopted before the test: " + permissionsBefore);
76                 }
77 
78                 // Gets all permissions, from test, test class, and superclasses
79                 ArraySet<String> permissions = new ArraySet<>();
80                 // Test itself
81                 addPermissions(permissions,
82                         description.getAnnotation(EnsureHasPermission.class));
83                 // Test class and superclasses
84                 Class<?> testClass = description.getTestClass();
85                 while (testClass != null) {
86                     addPermissions(permissions,
87                             testClass.getAnnotation(EnsureHasPermission.class));
88                     testClass = testClass.getSuperclass();
89                 }
90 
91                 if (permissions.isEmpty()) {
92                     if (DBG) {
93                         Log.d(TAG, "No annotation, running tests as-is");
94                     }
95                     base.evaluate();
96                     return;
97                 }
98 
99                 adoptShellPermissions(permissions, "Adopting Shell permissions before test: %s");
100                 try {
101                     base.evaluate();
102                 } finally {
103                     if (DBG) {
104                         Log.d(TAG, "Clearing shell permissions");
105                     }
106                     mUiAutomation.dropShellPermissionIdentity();
107                     adoptShellPermissions(permissionsBefore, "Restoring previous permissions: %s");
108                 }
109             }
110         };
111     } // apply()
112 
113     private void adoptShellPermissions(Set<String> permissionsSet, String messageTemplate) {
114         if (permissionsSet == null || permissionsSet.isEmpty()) {
115             return;
116         }
117         Log.d(TAG, String.format(messageTemplate, permissionsSet));
118         String[] permissions = permissionsSet.stream().toArray(n -> new String[n]);
119         mUiAutomation.adoptShellPermissionIdentity(permissions);
120     }
121 
122     private static void addPermissions(Set<String> permissions, EnsureHasPermission annotation) {
123         if (annotation == null) {
124             return;
125         }
126         for (String value : annotation.value()) {
127             permissions.add(value);
128         }
129     }
130 
131     // NOTE: ideally rule should use com.android.bedstead.harrier.annotations.EnsureHasPermission
132     // instead, but that annotation requires adding HarrierCommon, which causes other issues in the
133     // tests
134     /**
135      * Lists the permissions that will be adopted by a test method or class.
136      *
137      * <p>When defined by both method and class (or even superclasses), it will merge the
138      * permissions defined by such annotations.
139      */
140     @Retention(RUNTIME)
141     @Target({METHOD, TYPE})
142     public @interface EnsureHasPermission {
143 
144         /**
145          * List of permissions to be adopted by the test.
146          */
147         String[] value();
148     }
149 }
150