1 /*
2  * Copyright (C) 2011 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;
18 
19 import com.android.cts.verifier.PassFailButtons;
20 import com.android.cts.verifier.R;
21 import com.android.cts.verifier.TestResult;
22 
23 import android.app.AlertDialog;
24 import android.app.Dialog;
25 import android.app.PendingIntent;
26 import android.content.BroadcastReceiver;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.pm.PackageManager;
32 import android.content.res.Configuration;
33 import android.hardware.usb.UsbAccessory;
34 import android.hardware.usb.UsbManager;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.Message;
39 import android.os.ParcelFileDescriptor;
40 import android.util.Log;
41 import android.view.View;
42 import android.widget.ArrayAdapter;
43 import android.widget.ListView;
44 import android.widget.Toast;
45 
46 import java.io.FileDescriptor;
47 import java.io.FileInputStream;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.io.InputStream;
51 import java.io.OutputStream;
52 
53 /**
54  * Test for USB accessories. The test activity interacts with a cts-usb-accessory program that
55  * acts as an accessory by exchanging a series of messages.
56  */
57 public class UsbAccessoryTestActivity extends PassFailButtons.Activity {
58 
59     private static final String TAG = "UsbAccessoryTest";
60 
61     private static final int FILE_DESCRIPTOR_PROBLEM_DIALOG_ID = 1;
62     private static final int STATE_START = 0;
63     private static final int STATE_CONNECTED = 1;
64     private static final int STATE_WAITING_FOR_RECONNECT = 2;
65     private static final int STATE_RECONNECTED = 3;
66     private static final int STATE_PASSED = 4;
67 
68     private static final String ACTION_USB_PERMISSION =
69             "com.android.cts.verifier.usb.USB_PERMISSION";
70 
71     private ArrayAdapter<String> mReceivedMessagesAdapter;
72     private ArrayAdapter<String> mSentMessagesAdapter;
73     private MessageHandler mHandler;
74     private Handler mMainHandler;
75 
76     private UsbManager mUsbManager;
77     private PendingIntent mPermissionIntent;
78     private boolean mPermissionRequestPending;
79     private UsbReceiver mUsbReceiver;
80     private int mState = STATE_START;
81     private AlertDialog mDisconnectDialog;
82     private AlertDialog mConnectDialog;
83 
84     private UsbAccessory mAccessory;
85     private ParcelFileDescriptor mFileDescriptor;
86 
87     private Runnable mTimeoutRunnable = new Runnable() {
88         @Override
89         public void run() {
90             Toast.makeText(UsbAccessoryTestActivity.this,
91                     R.string.usb_reconnect_timeout, Toast.LENGTH_SHORT).show();
92             TestResult.setFailedResult(UsbAccessoryTestActivity.this, getTestId(), getTestDetails());
93         }
94     };
95 
96     @Override
onCreate(Bundle savedInstanceState)97     protected void onCreate(Bundle savedInstanceState) {
98         super.onCreate(savedInstanceState);
99         Log.d(TAG, "onCreate");
100         // Test success only works properly if launched from TestListActivity
101         String action = getIntent().getAction();
102         if (ACTION_USB_PERMISSION.equals(action)
103                 || UsbManager.ACTION_USB_ACCESSORY_ATTACHED.equals(action)) {
104             finish();
105             return;
106         }
107 
108         setContentView(R.layout.usb_main);
109         setInfoResources(R.string.usb_accessory_test, R.string.usb_accessory_test_info, -1);
110         setPassFailButtonClickListeners();
111 
112         // Don't allow a test pass until the accessory and the Android device exchange messages...
113         getPassButton().setEnabled(false);
114 
115         if (!hasUsbAccessorySupport()) {
116             showNoUsbAccessoryDialog();
117         }
118 
119         mReceivedMessagesAdapter = new ArrayAdapter<String>(this, R.layout.usb_message_row);
120         mSentMessagesAdapter = new ArrayAdapter<String>(this, R.layout.usb_message_row);
121         mHandler = new MessageHandler();
122 
123         mUsbManager = (UsbManager) getSystemService(USB_SERVICE);
124         mPermissionIntent = PendingIntent.getBroadcast(this, 0,
125                 new Intent(ACTION_USB_PERMISSION), 0);
126 
127         mUsbReceiver = new UsbReceiver();
128         IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
129         filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
130         registerReceiver(mUsbReceiver, filter);
131 
132         setupListViews();
133 
134         AlertDialog.Builder builder = new AlertDialog.Builder(this)
135             .setIcon(android.R.drawable.ic_dialog_alert)
136             .setTitle(R.string.usb_reconnect_title)
137             .setCancelable(false)
138             .setNegativeButton(R.string.usb_reconnect_abort,
139                     new DialogInterface.OnClickListener() {
140                 @Override
141                 public void onClick(DialogInterface dialog, int which) {
142                     setTestResultAndFinish(false);
143                 }
144             });
145         mConnectDialog = builder
146             .setMessage(R.string.usb_connect_message)
147             .create();
148         mDisconnectDialog = builder
149             .setMessage(R.string.usb_disconnect_message)
150             .create();
151 
152         mMainHandler = new Handler(Looper.getMainLooper());
153     }
154 
hasUsbAccessorySupport()155     private boolean hasUsbAccessorySupport() {
156         return getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_ACCESSORY);
157     }
158 
showNoUsbAccessoryDialog()159     private void showNoUsbAccessoryDialog() {
160         new AlertDialog.Builder(this)
161             .setIcon(android.R.drawable.ic_dialog_alert)
162             .setTitle(R.string.usb_not_available_title)
163             .setMessage(R.string.usb_not_available_message)
164             .setCancelable(false)
165             .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
166                 @Override
167                 public void onClick(DialogInterface dialog, int which) {
168                     finish();
169                 }
170             })
171             .show();
172     }
173 
setupListViews()174     private void setupListViews() {
175         ListView sentMessages = (ListView) findViewById(R.id.usb_sent_messages);
176         ListView receivedMessages = (ListView) findViewById(R.id.usb_received_messages);
177 
178         View emptySentView = findViewById(R.id.usb_empty_sent_messages);
179         View emptyReceivedView = findViewById(R.id.usb_empty_received_messages);
180         sentMessages.setEmptyView(emptySentView);
181         receivedMessages.setEmptyView(emptyReceivedView);
182 
183         receivedMessages.setAdapter(mReceivedMessagesAdapter);
184         sentMessages.setAdapter(mSentMessagesAdapter);
185     }
186 
187     class UsbReceiver extends BroadcastReceiver {
188         @Override
onReceive(Context context, Intent intent)189         public void onReceive(Context context, Intent intent) {
190             Log.d(TAG, "Received broadcast: intent=" + intent);
191             if (ACTION_USB_PERMISSION.equals(intent.getAction())) {
192                 if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
193                     UsbAccessory accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
194                     openAccessory(accessory);
195                 } else {
196                     Log.i(TAG, "Permission denied...");
197                 }
198                 mPermissionRequestPending = false;
199             } else if (mState == STATE_WAITING_FOR_RECONNECT &&
200                     UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(intent.getAction())) {
201                 UsbAccessory accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
202                 if (accessory.equals(mAccessory)) {
203                     closeAccessory();
204                     mDisconnectDialog.dismiss();
205                     mConnectDialog.show();
206                     mMainHandler.postDelayed(mTimeoutRunnable, 10000 /* 10 seconds */);
207                 }
208             }
209         }
210     }
211 
onNewIntent(Intent intent)212     public void onNewIntent(Intent intent) {
213         super.onNewIntent(intent);
214         Log.d(TAG, "onNewIntent: state=" + mState + ", intent=" + intent);
215         if (UsbManager.ACTION_USB_ACCESSORY_ATTACHED.equals(intent.getAction())) {
216             UsbAccessory accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
217             openAccessory(accessory);
218         }
219     }
220 
openAccessory(UsbAccessory accessory)221     private void openAccessory(UsbAccessory accessory) {
222         mAccessory = accessory;
223         mFileDescriptor = mUsbManager.openAccessory(accessory);
224         if (mState == STATE_START) {
225             setState(STATE_CONNECTED);
226         } else if (mState == STATE_WAITING_FOR_RECONNECT) {
227             setState(STATE_RECONNECTED);
228             mConnectDialog.dismiss();
229         }
230         if (mFileDescriptor != null) {
231             FileDescriptor fileDescriptor = mFileDescriptor.getFileDescriptor();
232             FileInputStream inputStream = new FileInputStream(fileDescriptor);
233             FileOutputStream outputStream = new FileOutputStream(fileDescriptor);
234             new MessageThread(inputStream, outputStream, mHandler).start();
235         } else {
236             showDialog(FILE_DESCRIPTOR_PROBLEM_DIALOG_ID);
237         }
238     }
239 
closeAccessory()240     private void closeAccessory() {
241         mAccessory = null;
242         if (mFileDescriptor != null) {
243             try {
244                 mFileDescriptor.close();
245             } catch (IOException e) {
246                 Log.e(TAG, "Exception while closing file descriptor", e);
247             } finally {
248                 mFileDescriptor = null;
249             }
250         }
251     }
252 
253     static class MessageThread extends Thread {
254 
255         private final InputStream mInputStream;
256 
257         private final OutputStream mOutputStream;
258 
259         private final MessageHandler mHandler;
260 
261         private int mNextMessageNumber = 0;
262 
MessageThread(InputStream inputStream, OutputStream outputStream, MessageHandler handler)263         MessageThread(InputStream inputStream, OutputStream outputStream, MessageHandler handler) {
264             this.mInputStream = inputStream;
265             this.mOutputStream = outputStream;
266             this.mHandler = handler;
267         }
268 
269         @Override
run()270         public void run() {
271             mHandler.sendEmptyMessage(MessageHandler.MESSAGE_THREAD_STARTING);
272 
273             try {
274                 // Wait a bit or else the messages can appear to quick and be confusing...
275                 Thread.sleep(2000);
276                 sendMessage();
277 
278                 // Wait for response and send message acks...
279                 int numRead = 0;
280                 byte[] buffer = new byte[16384];
281                 boolean done = false;
282                 while (numRead >= 0 && !done) {
283                     numRead = mInputStream.read(buffer);
284                     if (numRead > 0) {
285                         done = handleReceivedMessage(buffer, numRead);
286                     }
287                 }
288             } catch (IOException e) {
289                 Log.e(TAG, "Exception while reading from input stream", e);
290                 mHandler.sendEmptyMessage(MessageHandler.MESSAGE_THREAD_EXCEPTION);
291             } catch (InterruptedException e) {
292                 Log.e(TAG, "Exception while reading from input stream", e);
293                 mHandler.sendEmptyMessage(MessageHandler.MESSAGE_THREAD_EXCEPTION);
294             }
295             mHandler.sendEmptyMessage(MessageHandler.MESSAGE_THREAD_ENDING);
296         }
297 
handleReceivedMessage(byte[] buffer, int numRead)298         private boolean handleReceivedMessage(byte[] buffer, int numRead) throws IOException {
299             // TODO: Check the contents of the message?
300             String text = new String(buffer, 0, numRead).trim();
301             mHandler.sendReceivedMessage(text);
302 
303             // Send back a response..
304             if (mNextMessageNumber <= 10) {
305                 sendMessage();
306                 return false;
307             } else {
308                 mHandler.sendEmptyMessage(MessageHandler.STAGE_PASSED);
309                 return true;
310             }
311         }
312 
sendMessage()313         private void sendMessage() throws IOException {
314             String text = "Message from Android device #" + mNextMessageNumber++;
315             mOutputStream.write(text.getBytes());
316             mHandler.sendSentMessage(text);
317         }
318     }
319 
320     class MessageHandler extends Handler {
321 
322         static final int RECEIVED_MESSAGE = 1;
323 
324         static final int SENT_MESSAGE = 2;
325 
326         static final int MESSAGE_THREAD_STARTING = 3;
327 
328         static final int MESSAGE_THREAD_EXCEPTION = 4;
329 
330         static final int MESSAGE_THREAD_ENDING = 5;
331 
332         static final int STAGE_PASSED = 6;
333 
334         @Override
handleMessage(Message msg)335         public void handleMessage(Message msg) {
336             super.handleMessage(msg);
337             switch (msg.what) {
338                 case RECEIVED_MESSAGE:
339                     mReceivedMessagesAdapter.add((String) msg.obj);
340                     break;
341 
342                 case SENT_MESSAGE:
343                     mSentMessagesAdapter.add((String) msg.obj);
344                     break;
345 
346                 case MESSAGE_THREAD_STARTING:
347                     showToast(R.string.usb_message_thread_started);
348                     break;
349 
350                 case MESSAGE_THREAD_EXCEPTION:
351                     showToast(R.string.usb_message_thread_exception);
352                     break;
353 
354                 case MESSAGE_THREAD_ENDING:
355                     showToast(R.string.usb_message_thread_ended);
356                     break;
357 
358                 case STAGE_PASSED:
359                     if (mState == STATE_RECONNECTED) {
360                         showToast(R.string.usb_test_passed);
361                         getPassButton().setEnabled(true);
362                         setState(STATE_PASSED);
363                         mMainHandler.removeCallbacks(mTimeoutRunnable);
364                     } else if (mState == STATE_CONNECTED) {
365                         mDisconnectDialog.show();
366                         setState(STATE_WAITING_FOR_RECONNECT);
367                     }
368                     break;
369 
370                 default:
371                     throw new IllegalArgumentException("Bad message type: " + msg.what);
372             }
373         }
374 
showToast(int messageId)375         private void showToast(int messageId) {
376             Toast.makeText(UsbAccessoryTestActivity.this, messageId, Toast.LENGTH_SHORT).show();
377         }
378 
sendReceivedMessage(String text)379         void sendReceivedMessage(String text) {
380             Message message = Message.obtain(this, RECEIVED_MESSAGE);
381             message.obj = text;
382             sendMessage(message);
383         }
384 
sendSentMessage(String text)385         void sendSentMessage(String text) {
386             Message message = Message.obtain(this, SENT_MESSAGE);
387             message.obj = text;
388             sendMessage(message);
389         }
390     }
391 
392     @Override
onResume()393     protected void onResume() {
394         super.onResume();
395         Log.d(TAG, "onResume: state=" + stateToString(mState));
396         if (mState == STATE_START) {
397             UsbAccessory[] accessories = mUsbManager.getAccessoryList();
398             UsbAccessory accessory = accessories != null && accessories.length > 0
399                     ? accessories[0]
400                     : null;
401             if (accessory != null) {
402                 if (mUsbManager.hasPermission(accessory)) {
403                     openAccessory(accessory);
404                 } else {
405                     if (!mPermissionRequestPending) {
406                         mUsbManager.requestPermission(accessory, mPermissionIntent);
407                         mPermissionRequestPending = true;
408                     }
409                 }
410             }
411         }
412     }
413 
414     @Override
onPause()415     protected void onPause() {
416         super.onPause();
417         Log.d(TAG, "onPause: state=" + stateToString(mState));
418     }
419 
420     @Override
onStop()421     protected void onStop() {
422         super.onStop();
423         Log.d(TAG, "onStop: state=" + stateToString(mState));
424         closeAccessory();
425     }
426 
427     @Override
onConfigurationChanged(Configuration newConfig)428     public void onConfigurationChanged(Configuration newConfig) {
429         super.onConfigurationChanged(newConfig);
430         setContentView(R.layout.usb_main);
431         setupListViews();
432     }
433 
434     @Override
onCreateDialog(int id, Bundle args)435     public Dialog onCreateDialog(int id, Bundle args) {
436         switch (id) {
437             case FILE_DESCRIPTOR_PROBLEM_DIALOG_ID:
438                 return new AlertDialog.Builder(this)
439                     .setIcon(android.R.drawable.ic_dialog_alert)
440                     .setTitle(R.string.usb_accessory_test)
441                     .setMessage(R.string.usb_file_descriptor_error)
442                     .create();
443 
444             default:
445                 return super.onCreateDialog(id, args);
446         }
447     }
448 
449     @Override
onDestroy()450     protected void onDestroy() {
451         super.onDestroy();
452         Log.d(TAG, "onDestroy");
453         if (mUsbReceiver != null) {
454             unregisterReceiver(mUsbReceiver);
455         }
456     }
457 
setState(int newState)458     private void setState(int newState) {
459         Log.d(TAG, "Transition: " + stateToString(mState) + " -> " + stateToString(newState));
460         mState = newState;
461     }
462 
463 
stateToString(int state)464     private static String stateToString(int state) {
465         switch (state) {
466             case STATE_START: return "START";
467             case STATE_CONNECTED: return "CONNECTED";
468             case STATE_WAITING_FOR_RECONNECT: return "WAITING_FOR_RECONNECT";
469             case STATE_RECONNECTED: return "RECONNECTED";
470             case STATE_PASSED: return "PASSED";
471             default: return "UNKNOWN";
472         }
473     }
474 
475 }
476