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