blob: 2041be0c8bd0ceeebcfe6284e995ca0e44fc9b72 [file] [log] [blame]
/*
* Copyright (C) 2016 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.usb;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.mtp.MtpConstants;
import android.mtp.MtpDevice;
import android.mtp.MtpDeviceInfo;
import android.mtp.MtpEvent;
import android.mtp.MtpObjectInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.util.MutableInt;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.cts.verifier.PassFailButtons;
import com.android.cts.verifier.R;
import junit.framework.AssertionFailedError;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MtpHostTestActivity extends PassFailButtons.Activity implements Handler.Callback {
private static final int MESSAGE_PASS = 0;
private static final int MESSAGE_FAIL = 1;
private static final int MESSAGE_RUN = 2;
private static final int ITEM_STATE_PASS = 0;
private static final int ITEM_STATE_FAIL = 1;
private static final int ITEM_STATE_INDETERMINATE = 2;
/**
* Subclass for PTP.
*/
private static final int SUBCLASS_STILL_IMAGE_CAPTURE = 1;
/**
* Subclass for Android style MTP.
*/
private static final int SUBCLASS_MTP = 0xff;
/**
* Protocol for Picture Transfer Protocol (PIMA 15470).
*/
private static final int PROTOCOL_PICTURE_TRANSFER = 1;
/**
* Protocol for Android style MTP.
*/
private static final int PROTOCOL_MTP = 0;
private static final int RETRY_DELAY_MS = 1000;
private static final String ACTION_PERMISSION_GRANTED =
"com.android.cts.verifier.usb.ACTION_PERMISSION_GRANTED";
private static final String TEST_FILE_NAME = "CtsVerifierTest_testfile.txt";
private static final byte[] TEST_FILE_CONTENTS =
"This is a test file created by CTS verifier test.".getBytes(StandardCharsets.US_ASCII);
private final Handler mHandler = new Handler(this);
private int mStep;
private final ArrayList<TestItem> mItems = new ArrayList<>();
private UsbManager mUsbManager;
private BroadcastReceiver mReceiver;
private UsbDevice mUsbDevice;
private MtpDevice mMtpDevice;
private ExecutorService mExecutor;
private TextView mErrorText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mtp_host_activity);
setInfoResources(R.string.mtp_host_test, R.string.mtp_host_test_info, -1);
setPassFailButtonClickListeners();
final LayoutInflater inflater = getLayoutInflater();
final LinearLayout itemsView = (LinearLayout) findViewById(R.id.mtp_host_list);
mErrorText = (TextView) findViewById(R.id.error_text);
// Don't allow a test pass until all steps are passed.
getPassButton().setEnabled(false);
// Build test items.
mItems.add(new TestItem(
inflater,
R.string.mtp_host_device_lookup_message,
new int[] { R.id.next_item_button }));
mItems.add(new TestItem(
inflater,
R.string.mtp_host_grant_permission_message,
null));
mItems.add(new TestItem(
inflater,
R.string.mtp_host_test_read_event_message,
null));
mItems.add(new TestItem(
inflater,
R.string.mtp_host_test_send_object_message,
null));
mItems.add(new TestItem(
inflater,
R.string.mtp_host_test_notification_message,
new int[] { R.id.pass_item_button, R.id.fail_item_button }));
for (final TestItem item : mItems) {
itemsView.addView(item.view);
}
mExecutor = Executors.newSingleThreadExecutor();
mUsbManager = getSystemService(UsbManager.class);
mStep = 0;
mHandler.sendEmptyMessage(MESSAGE_RUN);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mReceiver != null) {
unregisterReceiver(mReceiver);
mReceiver = null;
}
}
@Override
public boolean handleMessage(Message msg) {
final TestItem item = mStep < mItems.size() ? mItems.get(mStep) : null;
switch (msg.what) {
case MESSAGE_RUN:
if (item == null) {
getPassButton().setEnabled(true);
return true;
}
item.setEnabled(true);
mExecutor.execute(new Runnable() {
private final int mCurrentStep = mStep;
@Override
public void run() {
try {
int i = mCurrentStep;
if (i-- == 0) stepFindMtpDevice();
if (i-- == 0) stepGrantPermission();
if (i-- == 0) stepTestReadEvent();
if (i-- == 0) stepTestSendObject();
if (i-- == 0) stepTestNotification();
mHandler.sendEmptyMessage(MESSAGE_PASS);
} catch (Exception | AssertionFailedError exception) {
mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_FAIL, exception));
}
}
});
break;
case MESSAGE_PASS:
item.setState(ITEM_STATE_PASS);
item.setEnabled(false);
mStep++;
mHandler.sendEmptyMessage(MESSAGE_RUN);
break;
case MESSAGE_FAIL:
item.setState(ITEM_STATE_FAIL);
item.setEnabled(false);
final StringWriter writer = new StringWriter();
final Throwable throwable = (Throwable) msg.obj;
throwable.printStackTrace(new PrintWriter(writer));
mErrorText.setText(writer.toString());
break;
}
return true;
}
private void stepFindMtpDevice() throws InterruptedException {
assertEquals(R.id.next_item_button, waitForButtonClick());
UsbDevice device = null;
for (final UsbDevice candidate : mUsbManager.getDeviceList().values()) {
if (isMtpDevice(candidate)) {
device = candidate;
break;
}
}
assertNotNull(device);
mUsbDevice = device;
}
private void stepGrantPermission() throws InterruptedException {
if (!mUsbManager.hasPermission(mUsbDevice)) {
final CountDownLatch latch = new CountDownLatch(1);
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
unregisterReceiver(this);
mReceiver = null;
latch.countDown();
}
};
registerReceiver(mReceiver, new IntentFilter(ACTION_PERMISSION_GRANTED));
mUsbManager.requestPermission(
mUsbDevice,
PendingIntent.getBroadcast(
MtpHostTestActivity.this, 0, new Intent(ACTION_PERMISSION_GRANTED), 0));
latch.await();
assertTrue(mUsbManager.hasPermission(mUsbDevice));
}
final UsbDeviceConnection connection = mUsbManager.openDevice(mUsbDevice);
assertNotNull(connection);
// Try to rob device ownership from other applications.
for (int i = 0; i < mUsbDevice.getInterfaceCount(); i++) {
connection.claimInterface(mUsbDevice.getInterface(i), true);
connection.releaseInterface(mUsbDevice.getInterface(i));
}
mMtpDevice = new MtpDevice(mUsbDevice);
assertTrue(mMtpDevice.open(connection));
assertTrue(mMtpDevice.getStorageIds().length > 0);
}
private void stepTestReadEvent() {
assertNotNull(mMtpDevice.getDeviceInfo().getEventsSupported());
assertTrue(mMtpDevice.getDeviceInfo().isEventSupported(MtpEvent.EVENT_OBJECT_ADDED));
mMtpDevice.getObjectHandles(0xFFFFFFFF, 0x0, 0x0);
while (true) {
MtpEvent event;
try {
event = mMtpDevice.readEvent(null);
} catch (IOException e) {
fail();
return;
}
if (event.getEventCode() == MtpEvent.EVENT_OBJECT_ADDED) {
break;
}
SystemClock.sleep(RETRY_DELAY_MS);
}
}
private void stepTestSendObject() throws IOException {
final MtpDeviceInfo deviceInfo = mMtpDevice.getDeviceInfo();
assertNotNull(deviceInfo.getOperationsSupported());
assertTrue(deviceInfo.isOperationSupported(MtpConstants.OPERATION_SEND_OBJECT_INFO));
assertTrue(deviceInfo.isOperationSupported(MtpConstants.OPERATION_SEND_OBJECT));
// Delete an existing test file that may be created by the test previously.
final int storageId = mMtpDevice.getStorageIds()[0];
for (final int objectHandle : mMtpDevice.getObjectHandles(
storageId, /* all format */ 0, /* Just under the root */ -1)) {
final MtpObjectInfo info = mMtpDevice.getObjectInfo(objectHandle);
if (TEST_FILE_NAME.equals(info.getName())) {
assertTrue(mMtpDevice.deleteObject(objectHandle));
}
}
final MtpObjectInfo info = new MtpObjectInfo.Builder()
.setStorageId(storageId)
.setName(TEST_FILE_NAME)
.setCompressedSize(TEST_FILE_CONTENTS.length)
.setFormat(MtpConstants.FORMAT_TEXT)
.setParent(-1)
.build();
final MtpObjectInfo newInfo = mMtpDevice.sendObjectInfo(info);
assertNotNull(newInfo);
assertTrue(newInfo.getObjectHandle() != -1);
final ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
try {
try (final ParcelFileDescriptor.AutoCloseOutputStream stream =
new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1])) {
stream.write(TEST_FILE_CONTENTS);
}
assertTrue(mMtpDevice.sendObject(
newInfo.getObjectHandle(),
newInfo.getCompressedSizeLong(),
pipes[0]));
} finally {
pipes[0].close();
}
}
private void stepTestNotification() throws InterruptedException {
assertEquals(R.id.pass_item_button, waitForButtonClick());
}
private int waitForButtonClick() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final MutableInt result = new MutableInt(-1);
mItems.get(mStep).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
result.value = v.getId();
latch.countDown();
}
});
latch.await();
return result.value;
}
private static boolean isMtpDevice(UsbDevice device) {
for (int i = 0; i < device.getInterfaceCount(); i++) {
final UsbInterface usbInterface = device.getInterface(i);
if ((usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE &&
usbInterface.getInterfaceSubclass() == SUBCLASS_STILL_IMAGE_CAPTURE &&
usbInterface.getInterfaceProtocol() == PROTOCOL_PICTURE_TRANSFER)) {
return true;
}
if (usbInterface.getInterfaceClass() == UsbConstants.USB_SUBCLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == SUBCLASS_MTP &&
usbInterface.getInterfaceProtocol() == PROTOCOL_MTP &&
"MTP".equals(usbInterface.getName())) {
return true;
}
}
return false;
}
private static class TestItem {
private final View view;
private final int[] buttons;
TestItem(LayoutInflater inflater,
int messageText,
int[] buttons) {
this.view = inflater.inflate(R.layout.mtp_host_item, null, false);
final TextView textView = (TextView) view.findViewById(R.id.instructions);
textView.setText(messageText);
this.buttons = buttons != null ? buttons : new int[0];
for (final int id : this.buttons) {
final Button button = (Button) view.findViewById(id);
button.setVisibility(View.VISIBLE);
button.setEnabled(false);
}
}
void setOnClickListener(OnClickListener listener) {
for (final int id : buttons) {
final Button button = (Button) view.findViewById(id);
button.setOnClickListener(listener);
}
}
void setEnabled(boolean value) {
for (final int id : buttons) {
final Button button = (Button) view.findViewById(id);
button.setEnabled(value);
}
}
Button getButton(int id) {
return (Button) view.findViewById(id);
}
void setState(int state) {
final ImageView imageView = (ImageView) view.findViewById(R.id.status);
switch (state) {
case ITEM_STATE_PASS:
imageView.setImageResource(R.drawable.fs_good);
break;
case ITEM_STATE_FAIL:
imageView.setImageResource(R.drawable.fs_error);
break;
case ITEM_STATE_INDETERMINATE:
imageView.setImageResource(R.drawable.fs_indeterminate);
break;
}
}
}
}