• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.virtualdevice.cts.common;
18 
19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.junit.Assert.fail;
24 import static org.junit.Assume.assumeTrue;
25 import static org.mockito.ArgumentMatchers.any;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.reset;
28 import static org.mockito.Mockito.timeout;
29 import static org.mockito.Mockito.verify;
30 
31 import android.app.role.RoleManager;
32 import android.companion.AssociationInfo;
33 import android.companion.AssociationRequest;
34 import android.companion.CompanionDeviceManager;
35 import android.content.Context;
36 import android.content.pm.PackageManager;
37 import android.os.Build;
38 import android.os.Process;
39 import android.util.Log;
40 
41 import androidx.annotation.ChecksSdkIntAtLeast;
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.test.filters.SdkSuppress;
45 
46 import com.android.compatibility.common.util.FeatureUtil;
47 import com.android.compatibility.common.util.SystemUtil;
48 
49 import org.junit.rules.ExternalResource;
50 import org.mockito.Mock;
51 import org.mockito.MockitoAnnotations;
52 
53 import java.util.List;
54 import java.util.Locale;
55 import java.util.Objects;
56 import java.util.concurrent.CountDownLatch;
57 import java.util.concurrent.Executor;
58 import java.util.concurrent.TimeUnit;
59 import java.util.function.Consumer;
60 
61 /**
62  * A test rule that creates a {@link CompanionDeviceManager} association with the instrumented
63  * package for the duration of the test.
64  */
65 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName = "VanillaIceCream")
66 class FakeAssociationRule extends ExternalResource {
67     private static final String TAG = "FakeAssociationRule";
68 
69     private static final String DEVICE_PROFILE = AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
70     private static final String DISPLAY_NAME = "CTS CDM VDM Association";
71     private static final String FAKE_ASSOCIATION_ADDRESS_FORMAT = "00:00:00:00:00:%02d";
72 
73     private static final int TIMEOUT_MS = 10000;
74 
75     private final Context mContext = getInstrumentation().getContext();
76 
77     private final Executor mCallbackExecutor = Runnable::run;
78     private final RoleManager mRoleManager = mContext.getSystemService(RoleManager.class);
79 
80     @Mock
81     private CompanionDeviceManager.OnAssociationsChangedListener mOnAssociationsChangedListener;
82 
83     private int mNextDeviceId = 0;
84 
85     private AssociationInfo mAssociationInfo;
86     private final CompanionDeviceManager mCompanionDeviceManager =
87             mContext.getSystemService(CompanionDeviceManager.class);
88 
createManagedAssociationApi35()89     private AssociationInfo createManagedAssociationApi35() {
90         String deviceAddress = String.format(Locale.getDefault(Locale.Category.FORMAT),
91                 FAKE_ASSOCIATION_ADDRESS_FORMAT, ++mNextDeviceId);
92         if (mNextDeviceId > 99) {
93             throw new IllegalArgumentException("At most 99 associations supported");
94         }
95 
96         Log.d(TAG, "Associations before shell cmd: "
97                 + mCompanionDeviceManager.getMyAssociations().size());
98         reset(mOnAssociationsChangedListener);
99         mRoleManager.setBypassingRoleQualification(true);
100         SystemUtil.runShellCommandOrThrow(String.format(Locale.getDefault(Locale.Category.FORMAT),
101                 "cmd companiondevice associate %d %s %s %s true",
102                 getInstrumentation().getContext().getUserId(),
103                 mContext.getPackageName(),
104                 deviceAddress,
105                 DEVICE_PROFILE));
106         verifyAssociationsChanged();
107         mRoleManager.setBypassingRoleQualification(false);
108 
109         // Immediately drop the role and rely on Shell
110         Consumer<Boolean> callback = mock(Consumer.class);
111         mRoleManager.removeRoleHolderAsUser(
112                 DEVICE_PROFILE, mContext.getPackageName(),
113                 RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, Process.myUserHandle(),
114                 mCallbackExecutor, callback);
115         verify(callback, timeout(TIMEOUT_MS)).accept(true);
116 
117         List<AssociationInfo> associations = mCompanionDeviceManager.getMyAssociations();
118 
119         final AssociationInfo associationInfo =
120                 associations.stream()
121                         .filter(a -> deviceAddress.equals(a.getDeviceMacAddress().toString()))
122                         .findAny()
123                         .orElse(null);
124         assertThat(associationInfo).isNotNull();
125         return associationInfo;
126     }
127 
createManagedAssociation(String deviceProfile)128     public AssociationInfo createManagedAssociation(String deviceProfile) {
129         final AssociationInfo[] managedAssociation = new AssociationInfo[1];
130         AssociationRequest request = new AssociationRequest.Builder()
131                 .setDeviceProfile(deviceProfile)
132                 .setDisplayName(DISPLAY_NAME + " - " + mNextDeviceId++)
133                 .setSelfManaged(true)
134                 .setSkipRoleGrant(true)
135                 .build();
136         CountDownLatch latch = new CountDownLatch(1);
137         CompanionDeviceManager.Callback callback  = new CompanionDeviceManager.Callback() {
138             @Override
139             public void onAssociationCreated(@NonNull AssociationInfo associationInfo) {
140                 managedAssociation[0] = associationInfo;
141                 latch.countDown();
142             }
143 
144             @Override
145             public void onFailure(@Nullable CharSequence error) {
146                 fail(error == null ? "Failed to create CDM association" : error.toString());
147             }
148         };
149         reset(mOnAssociationsChangedListener);
150         mCompanionDeviceManager.associate(request, Runnable::run, callback);
151 
152         try {
153             latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
154         } catch (InterruptedException e) {
155             fail("Interrupted while waiting for CDM association: " + e);
156         }
157 
158         verifyAssociationsChanged();
159         return managedAssociation[0];
160     }
161 
162     @Override
before()163     protected void before() throws Throwable {
164         super.before();
165         MockitoAnnotations.initMocks(this);
166         assumeTrue(FeatureUtil.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP));
167         mCompanionDeviceManager.addOnAssociationsChangedListener(
168                 mCallbackExecutor, mOnAssociationsChangedListener);
169         clearExistingAssociations();
170         if (isAtLeastB()) {
171             mAssociationInfo = createManagedAssociation(DEVICE_PROFILE);
172         } else {
173             mAssociationInfo = createManagedAssociationApi35();
174         }
175     }
176 
177     @Override
after()178     protected void after() {
179         super.after();
180         clearExistingAssociations();
181         mCompanionDeviceManager.removeOnAssociationsChangedListener(
182                 mOnAssociationsChangedListener);
183     }
184 
clearExistingAssociations()185     private void clearExistingAssociations() {
186         List<AssociationInfo> associations = mCompanionDeviceManager.getMyAssociations();
187         for (AssociationInfo association : associations) {
188             disassociate(association.getId());
189         }
190         assertThat(mCompanionDeviceManager.getMyAssociations()).isEmpty();
191         mAssociationInfo = null;
192     }
193 
getAssociationInfo()194     public AssociationInfo getAssociationInfo() {
195         return mAssociationInfo;
196     }
197 
disassociate()198     public void disassociate() {
199         clearExistingAssociations();
200     }
201 
disassociate(int associationId)202     private void disassociate(int associationId) {
203         reset(mOnAssociationsChangedListener);
204         mCompanionDeviceManager.disassociate(associationId);
205         verifyAssociationsChanged();
206     }
207 
verifyAssociationsChanged()208     private void verifyAssociationsChanged() {
209         verify(mOnAssociationsChangedListener, timeout(TIMEOUT_MS)
210                 .description(TAG + ": onAssociationChanged not called, total associations: "
211                         + mCompanionDeviceManager.getMyAssociations().size()))
212                 .onAssociationsChanged(any());
213     }
214 
215     @ChecksSdkIntAtLeast(api = 36 /* BUILD_VERSION_CODES.Baklava */)
isAtLeastB()216     private static boolean isAtLeastB() {
217         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA
218                 || Objects.equals(Build.VERSION.CODENAME, "Baklava");
219     }
220 }
221