1 /*
2  * Copyright (C) 2012 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.audiotest;
18 
19 import android.app.Activity;
20 import  android.media.AudioFormat;
21 import android.media.AudioManager;
22 import android.media.AudioRecord;
23 import android.media.MediaRecorder.AudioSource;
24 import android.media.AudioTrack;
25 import android.os.Build;
26 import android.os.Looper;
27 import android.util.Log;
28 
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.lang.Thread;
33 import java.net.ServerSocket;
34 import java.net.Socket;
35 import java.net.SocketTimeoutException;
36 import java.nio.ByteBuffer;
37 import java.util.HashMap;
38 import java.util.concurrent.locks.ReentrantLock;
39 
40 
41 public class AudioProtocol implements AudioTrack.OnPlaybackPositionUpdateListener {
42     private static final String TAG = "AudioProtocol";
43     private static final int PORT_NUMBER = 15001;
44 
45     private Thread mThread = new Thread(new ProtocolServer());
46     private boolean mExitRequested = false;
47 
48     private static final int PROTOCOL_HEADER_SIZE = 8; // id + payload length
49     private static final int MAX_NON_DATA_PAYLOAD_SIZE = 20;
50     private static final int PROTOCOL_SIMPLE_REPLY_SIZE = 12;
51     private static final int PROTOCOL_OK = 0;
52     private static final int PROTOCOL_ERROR_WRONG_PARAM = 1;
53     private static final int PROTOCOL_ERROR_GENERIC = 2;
54 
55     private static final int CMD_DOWNLOAD        = 0x12340001;
56     private static final int CMD_START_PLAYBACK  = 0x12340002;
57     private static final int CMD_STOP_PLAYBACK   = 0x12340003;
58     private static final int CMD_START_RECORDING = 0x12340004;
59     private static final int CMD_STOP_RECORDING  = 0x12340005;
60     private static final int CMD_GET_DEVICE_INFO = 0x12340006;
61 
62     private ByteBuffer mHeaderBuffer = ByteBuffer.allocate(PROTOCOL_HEADER_SIZE);
63     private ByteBuffer mDataBuffer = ByteBuffer.allocate(MAX_NON_DATA_PAYLOAD_SIZE);
64     private ByteBuffer mReplyBuffer = ByteBuffer.allocate(PROTOCOL_SIMPLE_REPLY_SIZE);
65 
66     // all socket access (accept / read) set this timeout to check exit periodically.
67     private static final int SOCKET_ACCESS_TIMEOUT = 2000;
68     private Socket mClient = null;
69     private InputStream mInput = null;
70     private OutputStream mOutput = null;
71     // lock to use to write to socket, I/O streams, and also change socket (create, destroy)
72     private ReentrantLock mClientLock = new ReentrantLock();
73 
74     private AudioRecord mRecord = null;
75     private LoopThread mRecordThread = null;
76     private AudioTrack mPlayback = null;
77     private LoopThread mPlaybackThread = null;
78     // store recording length
79     private int mRecordingLength = 0;
80 
81     // map for playback data
82     private HashMap<Integer, ByteBuffer> mDataMap = new HashMap<Integer, ByteBuffer>();
83 
start()84     public boolean start() {
85         Log.d(TAG, "start");
86         mExitRequested = false;
87         mThread.start();
88         //Log.d(TAG, "started");
89         return true;
90     }
91 
stop()92     public void stop() throws InterruptedException {
93         Log.d(TAG, "stop");
94         mExitRequested = true;
95         try {
96             mClientLock.lock();
97             if (mClient != null) {
98                 // wake up from socket read
99                 mClient.shutdownInput();
100             }
101         }catch (IOException e) {
102                 // ignore
103         } finally {
104             mClientLock.unlock();
105         }
106         mThread.interrupt(); // this does not bail out from socket in android
107         mThread.join();
108         reset();
109         Log.d(TAG, "stopped");
110     }
111 
112     @Override
onMarkerReached(AudioTrack track)113     public void onMarkerReached(AudioTrack track) {
114         Log.d(TAG, "playback completed");
115         track.stop();
116         track.flush();
117         track.release();
118         mPlaybackThread.quitLoop();
119         mPlaybackThread = null;
120         try {
121             sendSimpleReplyHeader(CMD_START_PLAYBACK, PROTOCOL_OK);
122         } catch (IOException e) {
123             // maybe socket already closed. don't do anything
124             Log.e(TAG, "ignore exception", e);
125         }
126     }
127 
128     @Override
onPeriodicNotification(AudioTrack arg0)129     public void onPeriodicNotification(AudioTrack arg0) {
130         Log.d(TAG, "track periodic notification");
131         // TODO Auto-generated method stub
132     }
133 
134     /**
135      * Read given amount of data to the buffer
136      * @param in
137      * @param buffer
138      * @param len length to read
139      * @return true if header read successfully, false if exit requested
140      * @throws IOException
141      * @throws ExitRequest
142      */
read(InputStream in, ByteBuffer buffer, int len)143     private void read(InputStream in, ByteBuffer buffer, int len) throws IOException, ExitRequest {
144         buffer.clear();
145         int totalRead = 0;
146         while (totalRead < len) {
147             int readNow = in.read(buffer.array(), totalRead, len - totalRead);
148             if (readNow < 0) { // end-of-stream, error
149                 Log.e(TAG, "read returned " + readNow);
150                 throw new IOException();
151             }
152             totalRead += readNow;
153             if(mExitRequested) {
154                 throw new ExitRequest();
155             }
156         }
157     }
158 
159     private class ProtocolError  extends Exception {
ProtocolError(String message)160         public ProtocolError(String message) {
161             super(message);
162         }
163     }
164 
165     private class ExitRequest extends Exception {
ExitRequest()166         public ExitRequest() {
167             super();
168         }
169     }
170 
assertProtocol(boolean cond, String message)171     private void assertProtocol(boolean cond, String message) throws ProtocolError {
172         if (!cond) {
173             throw new ProtocolError(message);
174         }
175     }
176 
reset()177     private void reset() {
178         // lock only when it is not already locked by this thread
179         if (mClientLock.getHoldCount() == 0) {
180             mClientLock.lock();
181         }
182         if (mClient != null) {
183             try {
184                 mClient.close();
185             } catch (IOException e) {
186                 // ignore
187             }
188             mClient = null;
189         }
190         mInput = null;
191         mOutput = null;
192         while (mClientLock.getHoldCount() > 0) {
193             mClientLock.unlock();
194         }
195         if (mRecord != null) {
196             if (mRecord.getState() != AudioRecord.STATE_UNINITIALIZED) {
197                 mRecord.stop();
198             }
199             mRecord.release();
200             mRecord = null;
201         }
202         if (mRecordThread != null) {
203             mRecordThread.quitLoop();
204             mRecordThread = null;
205         }
206         if (mPlayback != null) {
207             if (mPlayback.getState() != AudioTrack.STATE_UNINITIALIZED) {
208                 mPlayback.stop();
209                 mPlayback.flush();
210             }
211             mPlayback.release();
212             mPlayback = null;
213         }
214         if (mPlaybackThread != null) {
215             mPlaybackThread.quitLoop();
216             mPlaybackThread = null;
217         }
218         mDataMap.clear();
219     }
220 
handleDownload(int len)221     private void handleDownload(int len) throws IOException, ExitRequest {
222         read(mInput, mDataBuffer, 4); // only for id
223         Integer id  = new Integer(mDataBuffer.getInt(0));
224         int dataLength = len - 4;
225         ByteBuffer data = ByteBuffer.allocate(dataLength);
226         read(mInput, data, dataLength);
227         mDataMap.put(id, data);
228         Log.d(TAG, "downloaded data id " + id + " len " + dataLength);
229         sendSimpleReplyHeader(CMD_DOWNLOAD, PROTOCOL_OK);
230     }
231 
handleStartPlayback(int len)232     private void handleStartPlayback(int len) throws ProtocolError, IOException, ExitRequest {
233         // this error is too critical, so do not even send reply
234         assertProtocol(len == 20, "wrong payload len");
235         read(mInput, mDataBuffer, len);
236         final Integer id = new Integer(mDataBuffer.getInt(0));
237         final int samplingRate = mDataBuffer.getInt(1 * 4);
238         final boolean stereo = ((mDataBuffer.getInt(2 * 4) & 0x80000000) != 0);
239         final int mode = mDataBuffer.getInt(2 * 4) & 0x7fffffff;
240         final int volume = mDataBuffer.getInt(3 * 4);
241         final int repeat = mDataBuffer.getInt(4 * 4);
242         try {
243             final ByteBuffer data = mDataMap.get(id);
244             if (data == null) {
245                 throw new ProtocolError("wrong id");
246             }
247             if (samplingRate != 44100) {
248                 throw new ProtocolError("wrong rate");
249             }
250             //FIXME in MODE_STATIC, setNotificationMarkerPosition does not work with full length
251             mPlaybackThread = new LoopThread(new Runnable() {
252 
253                 @Override
254                 public void run() {
255                     if (mPlayback != null) {
256                         mPlayback.release();
257                         mPlayback = null;
258                     }
259                     // STREAM_VOICE_CALL activates different speaker.
260                     // use MUSIC mode to activate the louder speaker.
261                     int type = AudioManager.STREAM_MUSIC;
262                     int bufferSize = AudioTrack.getMinBufferSize(samplingRate,
263                             stereo ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO,
264                             AudioFormat.ENCODING_PCM_16BIT);
265                     bufferSize = bufferSize * 4;
266                     if (bufferSize < 256 * 1024) {
267                         bufferSize = 256 * 1024;
268                     }
269                     if (bufferSize > data.capacity()) {
270                         bufferSize = data.capacity();
271                     }
272                     mPlayback = new AudioTrack(type, samplingRate,
273                             stereo ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO,
274                             AudioFormat.ENCODING_PCM_16BIT, bufferSize,
275                             AudioTrack.MODE_STREAM);
276                     float minVolume = mPlayback.getMinVolume();
277                     float maxVolume = mPlayback.getMaxVolume();
278                     float newVolume = (maxVolume - minVolume) * volume / 100 + minVolume;
279                     mPlayback.setStereoVolume(newVolume, newVolume);
280                     Log.d(TAG, "setting volume " + newVolume + " max " + maxVolume +
281                             " min " + minVolume + " received " + volume);
282                     int dataWritten = 0;
283                     int dataToWrite = (bufferSize < data.capacity())? bufferSize : data.capacity();
284                     mPlayback.write(data.array(), 0, dataToWrite);
285                     dataWritten = dataToWrite;
286                     mPlayback.setPlaybackPositionUpdateListener(AudioProtocol.this);
287 
288                     int endMarker = data.capacity()/(stereo ? 4 : 2);
289                     int res = mPlayback.setNotificationMarkerPosition(endMarker);
290                     Log.d(TAG, "start playback id " + id + " len " + data.capacity() +
291                             " set.. res " + res + " stereo? " + stereo + " mode " + mode +
292                             " end " + endMarker);
293                     mPlayback.play();
294                     while (dataWritten < data.capacity()) {
295                         int dataLeft = data.capacity() - dataWritten;
296                         dataToWrite = (bufferSize < dataLeft)? bufferSize : dataLeft;
297                         if (mPlayback == null) { // stopped
298                             return;
299                         }
300                         mPlayback.write(data.array(), dataWritten, dataToWrite);
301                         dataWritten += dataToWrite;
302                     }
303                 }
304             });
305             mPlaybackThread.start();
306             // send reply when play is completed
307         } catch (ProtocolError e) {
308             sendSimpleReplyHeader(CMD_START_PLAYBACK, PROTOCOL_ERROR_WRONG_PARAM);
309             Log.e(TAG, "wrong param", e);
310         }
311     }
312 
handleStopPlayback(int len)313     private void handleStopPlayback(int len) throws ProtocolError, IOException {
314         Log.d(TAG, "stopPlayback");
315         assertProtocol(len == 0, "wrong payload len");
316         if (mPlayback != null) {
317             Log.d(TAG, "release AudioTrack");
318             mPlayback.stop();
319             mPlayback.flush();
320             mPlayback.release();
321             mPlayback = null;
322         }
323         if (mPlaybackThread != null) {
324             mPlaybackThread.quitLoop();
325             mPlaybackThread = null;
326         }
327         sendSimpleReplyHeader(CMD_STOP_PLAYBACK, PROTOCOL_OK);
328     }
329 
handleStartRecording(int len)330     private void handleStartRecording(int len) throws ProtocolError, IOException, ExitRequest {
331         assertProtocol(len == 16, "wrong payload len");
332         read(mInput, mDataBuffer, len);
333         final int samplingRate = mDataBuffer.getInt(0);
334         final boolean stereo = ((mDataBuffer.getInt(1 * 4) & 0x80000000) != 0);
335         final int mode = mDataBuffer.getInt(1 * 4) & 0x7fffffff;
336         final int volume = mDataBuffer.getInt(2 * 4);
337         final int samples = mDataBuffer.getInt(3 * 4);
338         try {
339             if (samplingRate != 44100) {
340                 throw new ProtocolError("wrong rate");
341             }
342             if (stereo) {
343                 throw new ProtocolError("mono only");
344             }
345             //TODO volume ?
346             mRecordingLength = samples * 2;
347             mRecordThread = new LoopThread(new Runnable() {
348 
349                 @Override
350                 public void run() {
351                     int minBufferSize = AudioRecord.getMinBufferSize(samplingRate,
352                             AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
353                     int type = (mode == 0) ? AudioSource.VOICE_RECOGNITION : AudioSource.DEFAULT;
354                     mRecord = new AudioRecord(type, samplingRate,
355                             AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT,
356                             (minBufferSize > mRecordingLength) ? minBufferSize : mRecordingLength);
357 
358                     mRecord.startRecording();
359                     Log.d(TAG, "recording started " + " samples " + samples + " mode " + mode +
360                             " recording state " + mRecord.getRecordingState() + " len " +
361                             mRecordingLength);
362                     try {
363                         boolean recordingOk = true;
364                         byte[] data = new byte[mRecordingLength];
365                         int totalRead = 0;
366                         while (totalRead < mRecordingLength) {
367                             int lenRead = mRecord.read(data, 0, (mRecordingLength - totalRead));
368                             if (lenRead < 0) {
369                                 Log.e(TAG, "reading recording failed with error code " + lenRead);
370                                 recordingOk = false;
371                                 break;
372                             } else if (lenRead == 0) {
373                                 Log.w(TAG, "zero read");
374                             }
375                             totalRead += lenRead;
376                         }
377                         Log.d(TAG, "reading recording completed");
378                         sendReplyWithData(
379                                 CMD_START_RECORDING,
380                                 recordingOk ? PROTOCOL_OK : PROTOCOL_ERROR_GENERIC,
381                                 recordingOk ? mRecordingLength : 0,
382                                 recordingOk ? data : null);
383                     } catch (IOException e) {
384                         // maybe socket already closed. don't do anything
385                         Log.e(TAG, "ignore exception", e);
386                     } finally {
387                         mRecord.stop();
388                         mRecord.release();
389                         mRecord = null;
390                     }
391                 }
392              });
393             mRecordThread.start();
394         } catch (ProtocolError e) {
395             sendSimpleReplyHeader(CMD_START_RECORDING, PROTOCOL_ERROR_WRONG_PARAM);
396             Log.e(TAG, "wrong param", e);
397         }
398     }
399 
handleStopRecording(int len)400     private void handleStopRecording(int len) throws ProtocolError, IOException {
401         Log.d(TAG, "stop recording");
402         assertProtocol(len == 0, "wrong payload len");
403         if (mRecord != null) {
404             mRecord.stop();
405             mRecord.release();
406             mRecord = null;
407         }
408         if (mRecordThread != null) {
409             mRecordThread.quitLoop();
410             mRecordThread = null;
411         }
412         sendSimpleReplyHeader(CMD_STOP_RECORDING, PROTOCOL_OK);
413     }
414 
415     private static final String BUILD_INFO_TAG = "build-info";
416 
appendAttrib(StringBuilder builder, String name, String value)417     private void appendAttrib(StringBuilder builder, String name, String value) {
418         builder.append(" " + name + "=\"" + value + "\"");
419     }
420 
handleGetDeviceInfo(int len)421     private void handleGetDeviceInfo(int len) throws ProtocolError, IOException{
422         Log.d(TAG, "getDeviceInfo");
423         assertProtocol(len == 0, "wrong payload len");
424         StringBuilder builder = new StringBuilder();
425         builder.append("<build-info");
426         appendAttrib(builder, "board", Build.BOARD);
427         appendAttrib(builder, "brand", Build.BRAND);
428         appendAttrib(builder, "device", Build.DEVICE);
429         appendAttrib(builder, "display", Build.DISPLAY);
430         appendAttrib(builder, "fingerprint", Build.FINGERPRINT);
431         appendAttrib(builder, "id", Build.ID);
432         appendAttrib(builder, "model", Build.MODEL);
433         appendAttrib(builder, "product", Build.PRODUCT);
434         appendAttrib(builder, "release", Build.VERSION.RELEASE);
435         appendAttrib(builder, "sdk", Integer.toString(Build.VERSION.SDK_INT));
436         builder.append(" />");
437         byte[] data = builder.toString().getBytes();
438 
439         sendReplyWithData(CMD_GET_DEVICE_INFO, PROTOCOL_OK, data.length, data);
440     }
441     /**
442      * send reply without payload.
443      * This function is thread-safe.
444      * @param out
445      * @param command
446      * @param errorCode
447      * @throws IOException
448      */
sendSimpleReplyHeader(int command, int errorCode)449     private void sendSimpleReplyHeader(int command, int errorCode) throws IOException {
450         Log.d(TAG, "sending reply cmd " + command + " err " + errorCode);
451         sendReplyWithData(command, errorCode, 0, null);
452     }
453 
sendReplyWithData(int cmd, int errorCode, int len, byte[] data)454     private void sendReplyWithData(int cmd, int errorCode, int len, byte[] data) throws IOException {
455         try {
456             mClientLock.lock();
457             mReplyBuffer.clear();
458             mReplyBuffer.putInt((cmd & 0xffff) | 0x43210000);
459             mReplyBuffer.putInt(errorCode);
460             mReplyBuffer.putInt(len);
461 
462             if (mOutput != null) {
463                 mOutput.write(mReplyBuffer.array(), 0, PROTOCOL_SIMPLE_REPLY_SIZE);
464                 if (data != null) {
465                     mOutput.write(data, 0, len);
466                 }
467             }
468         } catch (IOException e) {
469             throw e;
470         } finally {
471             mClientLock.unlock();
472         }
473     }
474     private class LoopThread extends Thread {
475         private Looper mLooper;
LoopThread(Runnable runnable)476         LoopThread(Runnable runnable) {
477             super(runnable);
478         }
run()479         public void run() {
480             Looper.prepare();
481             mLooper = Looper.myLooper();
482             Log.d(TAG, "run runnable");
483             super.run();
484             //Log.d(TAG, "loop");
485             Looper.loop();
486         }
487         // should be called outside this thread
quitLoop()488         public void quitLoop() {
489             mLooper.quit();
490             try {
491                 if (Thread.currentThread() != this) {
492                     join();
493                 }
494             } catch (InterruptedException e) {
495                 // ignore
496             }
497             Log.d(TAG, "quit thread");
498         }
499     }
500 
501     private class ProtocolServer implements Runnable {
502 
503         @Override
run()504         public void run() {
505             ServerSocket server = null;
506 
507             try { // for catching exception from ServerSocket
508                 Log.d(TAG, "get new server socket");
509                 server = new ServerSocket(PORT_NUMBER);
510                 server.setReuseAddress(true);
511                 server.setSoTimeout(SOCKET_ACCESS_TIMEOUT);
512                 while (!mExitRequested) {
513                     //TODO check already active recording/playback
514                     try { // for catching exception from Socket, will restart upon exception
515                         try {
516                             mClientLock.lock();
517                             //Log.d(TAG, "will accept");
518                             mClient = server.accept();
519                             mClient.setReuseAddress(true);
520                             mInput = mClient.getInputStream();
521                             mOutput = mClient.getOutputStream();
522                         } catch (SocketTimeoutException e) {
523                             // This will happen frequently if client does not connect.
524                             // just re-start
525                             continue;
526                         } finally {
527                             mClientLock.unlock();
528                         }
529                         Log.i(TAG, "new client connected");
530                         while (!mExitRequested) {
531                             read(mInput, mHeaderBuffer, PROTOCOL_HEADER_SIZE);
532                             int command = mHeaderBuffer.getInt();
533                             int len = mHeaderBuffer.getInt();
534                             Log.i(TAG, "received command " + command);
535                             switch(command) {
536                             case CMD_DOWNLOAD:
537                                 handleDownload(len);
538                                 break;
539                             case CMD_START_PLAYBACK:
540                                 handleStartPlayback(len);
541                                 break;
542                             case CMD_STOP_PLAYBACK:
543                                 handleStopPlayback(len);
544                                 break;
545                             case CMD_START_RECORDING:
546                                 handleStartRecording(len);
547                                 break;
548                             case CMD_STOP_RECORDING:
549                                 handleStopRecording(len);
550                                 break;
551                             case CMD_GET_DEVICE_INFO:
552                                 handleGetDeviceInfo(len);
553                             }
554                         }
555                     } catch (IOException e) {
556                         Log.e(TAG, "restart from exception", e);
557                     } catch (ProtocolError e) {
558                         Log.e(TAG, "restart from exception",  e);
559                     } finally {
560                         reset();
561                     }
562                 }
563             } catch (ExitRequest e) {
564                 Log.e(TAG, "exit requested, will exit", e);
565             } catch (IOException e) {
566                 // error in server socket, just exit the thread and let things fail.
567                 Log.e(TAG, "error while init, will exit", e);
568             } finally {
569                 if (server != null) {
570                     try {
571                         server.close();
572                     } catch (IOException e) {
573                         // ignore
574                     }
575                 }
576                 reset();
577             }
578         }
579     }
580 }
581