Home | History | Annotate | Download | only in mtp
      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