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