blob: 97ec07acf78694bca1551d629585bb6917c34896 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.verifier.companion;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.BluetoothDeviceFilter;
import android.companion.CompanionDeviceManager;
import android.companion.CompanionDeviceService;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.android.compatibility.common.util.CddTest;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
import java.util.ArrayList;
/**
* Test that Companion Device Awake {@link CompanionDeviceService} API is functional.
*/
@CddTest(requirements = {"3.16/C-1-2", "C-1-3", "H-1-1"})
public class CompanionDeviceServiceTestActivity extends PassFailButtons.Activity {
private static final String LOG_TAG = "CDMServiceTestActivity";
private static final int REQUEST_CODE_CHOOSER = 0;
private final ArrayList<TestStep> mTests = new ArrayList<>();
private CompanionDeviceManager mCompanionDeviceManager;
private TextView mTestTitle;
private TextView mTestDescription;
private ViewGroup mTestStepButtonLayout;
private Button mTestAction;
private Button mTestStepPassed;
private Button mTestStepFailed;
private int mCurrentTestIndex;
private AssociationInfo mCurrentAssociation;
// Test state verification loop will be launched on a new thread.
// Null until first verification is required.
private Runnable mVerifier;
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.companion_service_test_main);
mTestTitle = findViewById(R.id.companion_service_test_title);
mTestDescription = findViewById(R.id.companion_service_test_description);
mTestAction = findViewById(R.id.companion_service_test_button);
mTestStepButtonLayout = findViewById(R.id.button_layout);
mTestStepPassed = findViewById(R.id.test_step_passed);
mTestStepFailed = findViewById(R.id.test_step_failed);
mCurrentTestIndex = -1;
mCurrentAssociation = null;
mVerifier = null;
mHandler = new Handler(Looper.myLooper());
mCompanionDeviceManager = getSystemService(CompanionDeviceManager.class);
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) {
// Cannot move forward if bluetooth feature is not supported on the device.
mTests.add(new BluetoothFeatureTestStep());
} else {
// Add tests.
mTests.add(new DeviceAssociationTestStep());
mTests.add(new DevicePresentTestStep());
mTests.add(new DeviceGoneTestStep());
mTests.add(new DeviceDisassociationTestStep());
}
setPassFailButtonClickListeners();
getPassButton().setEnabled(false);
setInfoResources(R.string.companion_service_test, R.string.companion_service_test_info, -1);
runNextTestOrShowSummary();
cleanUp();
}
/**
* Get association info with matching ID. Returns null if no match.
*/
private AssociationInfo getAssociation(int id) {
for (AssociationInfo association : mCompanionDeviceManager.getMyAssociations()) {
if (association.getId() == id) return association;
}
return null;
}
/** Stop observing to associated device and then disassociate. */
private void disassociate(AssociationInfo association) {
String deviceAddress = association.getDeviceMacAddress().toString();
mCompanionDeviceManager.stopObservingDevicePresence(deviceAddress);
mCompanionDeviceManager.disassociate(association.getId());
Log.d(LOG_TAG, "Disassociated with device: " + deviceAddress);
}
/** Clean up any associated devices from this app. */
private void cleanUp() {
for (AssociationInfo association : mCompanionDeviceManager.getMyAssociations()) {
disassociate(association);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// On "associate()" success
if (requestCode == REQUEST_CODE_CHOOSER) {
if (resultCode != RESULT_OK) {
fail("Activity result code " + resultCode);
return;
}
AssociationInfo association =
data.getParcelableExtra(CompanionDeviceManager.EXTRA_ASSOCIATION,
AssociationInfo.class);
// This test is for bluetooth devices, which should all have a MAC address.
if (association == null || association.getDeviceMacAddress() == null) {
fail("The device was present but its address was null.");
return;
}
String deviceAddress = association.getDeviceMacAddress().toString();
mCompanionDeviceManager.startObservingDevicePresence(deviceAddress);
mCurrentAssociation = getAssociation(association.getId());
Log.d(LOG_TAG, "Associated with device: " + deviceAddress);
} else super.onActivityResult(requestCode, resultCode, data);
}
private void fail(Throwable reason) {
Log.e(LOG_TAG, "Test failed", reason);
fail(reason.getMessage());
}
private void fail(CharSequence reason) {
Toast.makeText(this, reason, Toast.LENGTH_LONG).show();
Log.e(LOG_TAG, reason.toString());
cleanUp();
setTestResultAndFinish(false);
}
private TestStep getCurrentTest() {
return mTests.get(mCurrentTestIndex);
}
private void runNextTestOrShowSummary() {
if (mCurrentTestIndex + 1 >= mTests.size()) {
updateViewForCompletionSummary();
} else {
mCurrentTestIndex++;
updateViewForTest(getCurrentTest());
}
}
/** Populates the UI based on the provided test step. */
private void updateViewForTest(TestStep test) {
mTestStepButtonLayout.setVisibility(VISIBLE);
mTestTitle.setText(test.mTitleResId);
mTestDescription.setText(test.mDescriptionResId);
// Can't pass until test result is verified.
mTestStepPassed.setEnabled(false);
mTestStepPassed.setOnClickListener(v -> getCurrentTest().onPass());
mTestStepFailed.setOnClickListener(v -> getCurrentTest().onFail());
mTestAction.setOnClickListener(v -> getCurrentTest().performTestAction());
if (mVerifier != null) {
mHandler.removeCallbacks(mVerifier);
}
// Display test action button if specified.
if (test.mButtonTextResId != 0) {
mTestAction.setText(test.mButtonTextResId);
mTestAction.setVisibility(VISIBLE);
} else {
mTestAction.setVisibility(INVISIBLE);
}
// Wait for test verification.
mVerifier = new Runnable() {
@Override
public void run() {
if (test.verify()) {
mTestStepPassed.setEnabled(true);
} else {
mHandler.postDelayed(this, 3000);
}
}
};
mHandler.postDelayed(mVerifier, 1000);
}
/** Populates the UI indicating results of test & updates test buttons as needed */
private void updateViewForCompletionSummary() {
// No longer need any of these buttons
mTestStepButtonLayout.setVisibility(INVISIBLE);
mTestAction.setVisibility(INVISIBLE);
// Can only reach here if all other tests passed. Enable pass button.
getPassButton().setEnabled(true);
mTestTitle.setText(R.string.companion_service_test_summary_title);
mTestDescription.setText(R.string.companion_service_test_summary);
}
/**
* This activity specifically tests for CDM interactions with bluetooth devices.
* Pass the test if device does not support bluetooth.
*/
private class BluetoothFeatureTestStep extends TestStep {
BluetoothFeatureTestStep() {
super(R.string.companion_service_bluetooth_feature_title,
R.string.companion_service_bluetooth_feature_text);
}
@Override
boolean verify() {
return true;
}
}
/**
* Tests that an association can be made with a device and that the app can subscribe
* to its presence.
*/
private class DeviceAssociationTestStep extends TestStep {
final CompanionDeviceManager.Callback mCallback =
new CompanionDeviceManager.Callback() {
@Override
public void onAssociationPending(IntentSender chooserLauncher) {
try {
startIntentSenderForResult(chooserLauncher,
REQUEST_CODE_CHOOSER, null, 0, 0, 0);
} catch (IntentSender.SendIntentException error) {
fail(error);
}
}
@Override
public void onFailure(CharSequence error) {
fail(error);
}
};
DeviceAssociationTestStep() {
super(R.string.companion_service_associate_title,
R.string.companion_service_associate_text,
R.string.companion_service_associate_button);
}
@Override
void performTestAction() {
AssociationRequest request = new AssociationRequest.Builder()
.addDeviceFilter(new BluetoothDeviceFilter.Builder().build())
.build();
mCompanionDeviceManager.associate(request, mCallback, null);
}
@Override
boolean verify() {
// Check that it is associated and being observed.
// Bypass inaccessible AssociationInfo#isNotifyOnDeviceNearby() with toString()
return mCurrentAssociation != null
&& mCurrentAssociation.toString().contains("mNotifyOnDeviceNearby=true");
}
}
/**
* Tests that app can correctly detect associated device's presence.
*/
private class DevicePresentTestStep extends TestStep {
DevicePresentTestStep() {
super(R.string.companion_service_present_title,
R.string.companion_service_present_text);
}
@Override
boolean verify() {
return DevicePresenceListener.isDeviceNearby(mCurrentAssociation.getId());
}
}
/**
* Tests that app can correctly detect device's disappearance.
*/
private class DeviceGoneTestStep extends TestStep {
DeviceGoneTestStep() {
super(R.string.companion_service_gone_title,
R.string.companion_service_gone_text);
}
@Override
public boolean verify() {
return !DevicePresenceListener.isDeviceNearby(mCurrentAssociation.getId());
}
}
/**
* Tests that device can be correctly disassociated from the app.
*/
private class DeviceDisassociationTestStep extends TestStep {
DeviceDisassociationTestStep() {
super(R.string.companion_service_disassociate_title,
R.string.companion_service_disassociate_text,
R.string.companion_service_disassociate_button);
}
@Override
void performTestAction() {
disassociate(mCurrentAssociation);
}
@Override
boolean verify() {
// Check that it is no longer associated.
return getAssociation(mCurrentAssociation.getId()) == null;
}
}
/**
* Interface for individual test steps.
*/
private abstract class TestStep {
final int mTitleResId;
final int mDescriptionResId;
final int mButtonTextResId;
TestStep(int titleResId, int descriptionResId) {
this(titleResId, descriptionResId, 0);
}
TestStep(int titleResId, int descriptionResId, int buttonTextResId) {
this.mTitleResId = titleResId;
this.mDescriptionResId = descriptionResId;
this.mButtonTextResId = buttonTextResId;
}
/** Code to run when the button is activated; only used if {@link #mButtonTextResId} != 0 */
void performTestAction() {
// optional
}
/** Checks device state to see if the test passed. */
abstract boolean verify();
/**
* Code to run on failure.
*/
void onPass() {
runNextTestOrShowSummary();
}
/**
* Code to run on failure.
*/
void onFail() {
cleanUp();
fail("Test failed manually.");
}
}
}