1 /* 2 * Copyright (C) 2016 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 com.android.cts.verifier.usb.mtp; 18 19 import static junit.framework.Assert.assertEquals; 20 import static junit.framework.Assert.assertNotNull; 21 import static junit.framework.Assert.assertTrue; 22 import static junit.framework.Assert.fail; 23 24 import android.app.PendingIntent; 25 import android.content.BroadcastReceiver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.hardware.usb.UsbConstants; 30 import android.hardware.usb.UsbDevice; 31 import android.hardware.usb.UsbDeviceConnection; 32 import android.hardware.usb.UsbInterface; 33 import android.hardware.usb.UsbManager; 34 import android.mtp.MtpConstants; 35 import android.mtp.MtpDevice; 36 import android.mtp.MtpDeviceInfo; 37 import android.mtp.MtpEvent; 38 import android.mtp.MtpObjectInfo; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.Message; 42 import android.os.ParcelFileDescriptor; 43 import android.os.SystemClock; 44 import android.provider.Settings; 45 import android.util.MutableInt; 46 import android.view.LayoutInflater; 47 import android.view.View; 48 import android.view.View.OnClickListener; 49 import android.widget.Button; 50 import android.widget.ImageView; 51 import android.widget.LinearLayout; 52 import android.widget.TextView; 53 54 import com.android.cts.verifier.PassFailButtons; 55 import com.android.cts.verifier.R; 56 57 import junit.framework.AssertionFailedError; 58 59 import java.io.IOException; 60 import java.io.PrintWriter; 61 import java.io.StringWriter; 62 import java.nio.charset.StandardCharsets; 63 import java.util.ArrayList; 64 import java.util.concurrent.CountDownLatch; 65 import java.util.concurrent.ExecutorService; 66 import java.util.concurrent.Executors; 67 68 public class MtpHostTestActivity extends PassFailButtons.Activity implements Handler.Callback { 69 private static final int MESSAGE_PASS = 0; 70 private static final int MESSAGE_FAIL = 1; 71 private static final int MESSAGE_RUN = 2; 72 73 private static final int ITEM_STATE_PASS = 0; 74 private static final int ITEM_STATE_FAIL = 1; 75 private static final int ITEM_STATE_INDETERMINATE = 2; 76 77 /** 78 * Subclass for PTP. 79 */ 80 private static final int SUBCLASS_STILL_IMAGE_CAPTURE = 1; 81 82 /** 83 * Subclass for Android style MTP. 84 */ 85 private static final int SUBCLASS_MTP = 0xff; 86 87 /** 88 * Protocol for Picture Transfer Protocol (PIMA 15470). 89 */ 90 private static final int PROTOCOL_PICTURE_TRANSFER = 1; 91 92 /** 93 * Protocol for Android style MTP. 94 */ 95 private static final int PROTOCOL_MTP = 0; 96 97 private static final int RETRY_DELAY_MS = 1000; 98 99 private static final String ACTION_PERMISSION_GRANTED = 100 "com.android.cts.verifier.usb.ACTION_PERMISSION_GRANTED"; 101 102 private static final String TEST_FILE_NAME = "CtsVerifierTest_testfile.txt"; 103 private static final byte[] TEST_FILE_CONTENTS = 104 "This is a test file created by CTS verifier test.".getBytes(StandardCharsets.US_ASCII); 105 106 private final Handler mHandler = new Handler(this); 107 private int mStep; 108 private final ArrayList<TestItem> mItems = new ArrayList<>(); 109 110 private UsbManager mUsbManager; 111 private BroadcastReceiver mReceiver; 112 private UsbDevice mUsbDevice; 113 private MtpDevice mMtpDevice; 114 private ExecutorService mExecutor; 115 private TextView mErrorText; 116 117 @Override 118 protected void onCreate(Bundle savedInstanceState) { 119 super.onCreate(savedInstanceState); 120 setContentView(R.layout.mtp_host_activity); 121 setInfoResources(R.string.mtp_host_test, R.string.mtp_host_test_info, -1); 122 setPassFailButtonClickListeners(); 123 124 final LayoutInflater inflater = getLayoutInflater(); 125 final LinearLayout itemsView = (LinearLayout) findViewById(R.id.mtp_host_list); 126 127 mErrorText = (TextView) findViewById(R.id.error_text); 128 129 // Don't allow a test pass until all steps are passed. 130 getPassButton().setEnabled(false); 131 132 // Build test items. 133 mItems.add(new TestItem( 134 inflater, 135 R.string.mtp_host_device_lookup_message, 136 new int[] { R.id.next_item_button })); 137 mItems.add(new TestItem( 138 inflater, 139 R.string.mtp_host_test_file_browse_message, 140 new int[] { R.id.settings_button, R.id.pass_item_button, R.id.fail_item_button })); 141 mItems.add(new TestItem( 142 inflater, 143 R.string.mtp_host_grant_permission_message, 144 null)); 145 mItems.add(new TestItem( 146 inflater, 147 R.string.mtp_host_test_read_event_message, 148 null)); 149 mItems.add(new TestItem( 150 inflater, 151 R.string.mtp_host_test_send_object_message, 152 null)); 153 for (final TestItem item : mItems) { 154 itemsView.addView(item.view); 155 } 156 157 mExecutor = Executors.newSingleThreadExecutor(); 158 mUsbManager = getSystemService(UsbManager.class); 159 160 mStep = 0; 161 mHandler.sendEmptyMessage(MESSAGE_RUN); 162 } 163 164 @Override 165 protected void onDestroy() { 166 super.onDestroy(); 167 if (mReceiver != null) { 168 unregisterReceiver(mReceiver); 169 mReceiver = null; 170 } 171 } 172 173 @Override 174 public boolean handleMessage(Message msg) { 175 final TestItem item = mStep < mItems.size() ? mItems.get(mStep) : null; 176 177 switch (msg.what) { 178 case MESSAGE_RUN: 179 if (item == null) { 180 getPassButton().setEnabled(true); 181 return true; 182 } 183 item.setEnabled(true); 184 mExecutor.execute(new Runnable() { 185 private final int mCurrentStep = mStep; 186 187 @Override 188 public void run() { 189 try { 190 switch (mCurrentStep) { 191 case 0: 192 stepFindMtpDevice(); 193 break; 194 case 1: 195 stepTestFileBrowse(); 196 break; 197 case 2: 198 stepGrantPermission(); 199 break; 200 case 3: 201 stepTestReadEvent(); 202 break; 203 case 4: 204 stepTestSendObject(); 205 break; 206 } 207 mHandler.sendEmptyMessage(MESSAGE_PASS); 208 } catch (Exception | AssertionFailedError exception) { 209 mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_FAIL, exception)); 210 } 211 } 212 }); 213 break; 214 215 case MESSAGE_PASS: 216 item.setState(ITEM_STATE_PASS); 217 item.setEnabled(false); 218 mStep++; 219 mHandler.sendEmptyMessage(MESSAGE_RUN); 220 break; 221 222 case MESSAGE_FAIL: 223 item.setState(ITEM_STATE_FAIL); 224 item.setEnabled(false); 225 final StringWriter writer = new StringWriter(); 226 final Throwable throwable = (Throwable) msg.obj; 227 throwable.printStackTrace(new PrintWriter(writer)); 228 mErrorText.setText(writer.toString()); 229 break; 230 } 231 232 return true; 233 } 234 235 private void stepFindMtpDevice() throws InterruptedException { 236 assertEquals(R.id.next_item_button, waitForButtonClick()); 237 238 UsbDevice device = null; 239 for (final UsbDevice candidate : mUsbManager.getDeviceList().values()) { 240 if (isMtpDevice(candidate)) { 241 device = candidate; 242 break; 243 } 244 } 245 assertNotNull(device); 246 mUsbDevice = device; 247 } 248 249 private void stepGrantPermission() throws InterruptedException { 250 if (!mUsbManager.hasPermission(mUsbDevice)) { 251 final CountDownLatch latch = new CountDownLatch(1); 252 mReceiver = new BroadcastReceiver() { 253 @Override 254 public void onReceive(Context context, Intent intent) { 255 unregisterReceiver(this); 256 mReceiver = null; 257 latch.countDown(); 258 } 259 }; 260 registerReceiver(mReceiver, new IntentFilter(ACTION_PERMISSION_GRANTED)); 261 mUsbManager.requestPermission( 262 mUsbDevice, 263 PendingIntent.getBroadcast( 264 MtpHostTestActivity.this, 0, new Intent(ACTION_PERMISSION_GRANTED), 0)); 265 266 latch.await(); 267 assertTrue(mUsbManager.hasPermission(mUsbDevice)); 268 } 269 270 final UsbDeviceConnection connection = mUsbManager.openDevice(mUsbDevice); 271 assertNotNull(connection); 272 273 // Try to rob device ownership from other applications. 274 for (int i = 0; i < mUsbDevice.getInterfaceCount(); i++) { 275 connection.claimInterface(mUsbDevice.getInterface(i), true); 276 connection.releaseInterface(mUsbDevice.getInterface(i)); 277 } 278 mMtpDevice = new MtpDevice(mUsbDevice); 279 assertTrue(mMtpDevice.open(connection)); 280 assertTrue(mMtpDevice.getStorageIds().length > 0); 281 } 282 283 private void stepTestReadEvent() { 284 assertNotNull(mMtpDevice.getDeviceInfo().getEventsSupported()); 285 assertTrue(mMtpDevice.getDeviceInfo().isEventSupported(MtpEvent.EVENT_OBJECT_ADDED)); 286 287 mMtpDevice.getObjectHandles(0xFFFFFFFF, 0x0, 0x0); 288 while (true) { 289 MtpEvent event; 290 try { 291 event = mMtpDevice.readEvent(null); 292 } catch (IOException e) { 293 fail(); 294 return; 295 } 296 if (event.getEventCode() == MtpEvent.EVENT_OBJECT_ADDED) { 297 break; 298 } 299 SystemClock.sleep(RETRY_DELAY_MS); 300 } 301 } 302 303 private void stepTestSendObject() throws IOException { 304 final MtpDeviceInfo deviceInfo = mMtpDevice.getDeviceInfo(); 305 assertNotNull(deviceInfo.getOperationsSupported()); 306 assertTrue(deviceInfo.isOperationSupported(MtpConstants.OPERATION_SEND_OBJECT_INFO)); 307 assertTrue(deviceInfo.isOperationSupported(MtpConstants.OPERATION_SEND_OBJECT)); 308 309 // Delete an existing test file that may be created by the test previously. 310 final int storageId = mMtpDevice.getStorageIds()[0]; 311 for (final int objectHandle : mMtpDevice.getObjectHandles( 312 storageId, /* all format */ 0, /* Just under the root */ -1)) { 313 final MtpObjectInfo info = mMtpDevice.getObjectInfo(objectHandle); 314 if (TEST_FILE_NAME.equals(info.getName())) { 315 assertTrue(mMtpDevice.deleteObject(objectHandle)); 316 } 317 } 318 319 final MtpObjectInfo info = new MtpObjectInfo.Builder() 320 .setStorageId(storageId) 321 .setName(TEST_FILE_NAME) 322 .setCompressedSize(TEST_FILE_CONTENTS.length) 323 .setFormat(MtpConstants.FORMAT_TEXT) 324 .setParent(-1) 325 .build(); 326 final MtpObjectInfo newInfo = mMtpDevice.sendObjectInfo(info); 327 assertNotNull(newInfo); 328 assertTrue(newInfo.getObjectHandle() != -1); 329 330 final ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe(); 331 try { 332 try (final ParcelFileDescriptor.AutoCloseOutputStream stream = 333 new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1])) { 334 stream.write(TEST_FILE_CONTENTS); 335 } 336 assertTrue(mMtpDevice.sendObject( 337 newInfo.getObjectHandle(), 338 newInfo.getCompressedSizeLong(), 339 pipes[0])); 340 } finally { 341 pipes[0].close(); 342 } 343 } 344 345 private void stepTestFileBrowse() throws InterruptedException { 346 while (true) { 347 final int id = waitForButtonClick(); 348 if (id == R.id.settings_button) { 349 startActivity(new Intent(Settings.ACTION_APPLICATION_SETTINGS)); 350 continue; 351 } 352 assertEquals(R.id.pass_item_button, waitForButtonClick()); 353 break; 354 } 355 } 356 357 private int waitForButtonClick() throws InterruptedException { 358 final CountDownLatch latch = new CountDownLatch(1); 359 final MutableInt result = new MutableInt(-1); 360 mItems.get(mStep).setOnClickListener(new OnClickListener() { 361 @Override 362 public void onClick(View v) { 363 result.value = v.getId(); 364 latch.countDown(); 365 } 366 }); 367 latch.await(); 368 return result.value; 369 } 370 371 private static boolean isMtpDevice(UsbDevice device) { 372 for (int i = 0; i < device.getInterfaceCount(); i++) { 373 final UsbInterface usbInterface = device.getInterface(i); 374 if ((usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE && 375 usbInterface.getInterfaceSubclass() == SUBCLASS_STILL_IMAGE_CAPTURE && 376 usbInterface.getInterfaceProtocol() == PROTOCOL_PICTURE_TRANSFER)) { 377 return true; 378 } 379 if (usbInterface.getInterfaceClass() == UsbConstants.USB_SUBCLASS_VENDOR_SPEC && 380 usbInterface.getInterfaceSubclass() == SUBCLASS_MTP && 381 usbInterface.getInterfaceProtocol() == PROTOCOL_MTP && 382 "MTP".equals(usbInterface.getName())) { 383 return true; 384 } 385 } 386 return false; 387 } 388 389 private static class TestItem { 390 private final View view; 391 private final int[] buttons; 392 393 TestItem(LayoutInflater inflater, 394 int messageText, 395 int[] buttons) { 396 this.view = inflater.inflate(R.layout.mtp_host_item, null, false); 397 398 final TextView textView = (TextView) view.findViewById(R.id.instructions); 399 textView.setText(messageText); 400 401 this.buttons = buttons != null ? buttons : new int[0]; 402 for (final int id : this.buttons) { 403 final Button button = (Button) view.findViewById(id); 404 button.setVisibility(View.VISIBLE); 405 button.setEnabled(false); 406 } 407 } 408 409 void setOnClickListener(OnClickListener listener) { 410 for (final int id : buttons) { 411 final Button button = (Button) view.findViewById(id); 412 button.setOnClickListener(listener); 413 } 414 } 415 416 void setEnabled(boolean value) { 417 for (final int id : buttons) { 418 final Button button = (Button) view.findViewById(id); 419 button.setEnabled(value); 420 } 421 } 422 423 Button getButton(int id) { 424 return (Button) view.findViewById(id); 425 } 426 427 void setState(int state) { 428 final ImageView imageView = (ImageView) view.findViewById(R.id.status); 429 switch (state) { 430 case ITEM_STATE_PASS: 431 imageView.setImageResource(R.drawable.fs_good); 432 break; 433 case ITEM_STATE_FAIL: 434 imageView.setImageResource(R.drawable.fs_error); 435 break; 436 case ITEM_STATE_INDETERMINATE: 437 imageView.setImageResource(R.drawable.fs_indeterminate); 438 break; 439 } 440 } 441 } 442 } 443