1 /*
2  * Copyright (C) 2021 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 android.voiceinteraction.service;
18 
19 import static android.media.AudioFormat.CHANNEL_IN_FRONT;
20 
21 import android.media.AudioAttributes;
22 import android.media.AudioFormat;
23 import android.media.AudioRecord;
24 import android.media.MediaRecorder;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.os.ParcelFileDescriptor;
28 import android.os.PersistableBundle;
29 import android.os.Process;
30 import android.os.SharedMemory;
31 import android.os.SystemClock;
32 import android.service.voice.AlwaysOnHotwordDetector;
33 import android.service.voice.HotwordAudioStream;
34 import android.service.voice.HotwordDetectedResult;
35 import android.service.voice.HotwordDetectionService;
36 import android.service.voice.HotwordRejectedResult;
37 import android.service.voice.SandboxedDetectionInitializer;
38 import android.system.ErrnoException;
39 import android.text.TextUtils;
40 import android.util.Log;
41 import android.voiceinteraction.common.Utils;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
49 import java.util.function.IntConsumer;
50 
51 import javax.annotation.concurrent.GuardedBy;
52 
53 public class MainHotwordDetectionService extends HotwordDetectionService {
54     static final String TAG = "MainHotwordDetectionService";
55 
56     public static byte[] FAKE_HOTWORD_AUDIO_DATA =
57             new byte[]{'h', 'o', 't', 'w', 'o', 'r', 'd', '!'};
58     public static String KEY_FAKE_DATA = "fakeData";
59     public static String VALUE_FAKE_DATA = "fakeData";
60     public static final int DEFAULT_PHRASE_ID = 5;
61     public static final HotwordDetectedResult DETECTED_RESULT =
62             new HotwordDetectedResult.Builder()
63                     .setAudioChannel(CHANNEL_IN_FRONT)
64                     .setConfidenceLevel(HotwordDetectedResult.CONFIDENCE_LEVEL_HIGH)
65                     .setHotwordDetectionPersonalized(true)
66                     .setHotwordDurationMillis(1000)
67                     .setHotwordOffsetMillis(500)
68                     .setHotwordPhraseId(DEFAULT_PHRASE_ID)
69                     .setPersonalizedScore(10)
70                     .setScore(15)
71                     .setBackgroundAudioPower(50)
72                     .build();
73     public static final HotwordDetectedResult DETECTED_RESULT_AFTER_STOP_DETECTION =
74             new HotwordDetectedResult.Builder()
75                     .setHotwordPhraseId(DEFAULT_PHRASE_ID)
76                     .setScore(57)
77                     .build();
78     public static final HotwordDetectedResult DETECTED_RESULT_FOR_MIC_FAILURE =
79             new HotwordDetectedResult.Builder()
80                     .setHotwordPhraseId(DEFAULT_PHRASE_ID)
81                     .setScore(58)
82                     .build();
83     public static final HotwordRejectedResult REJECTED_RESULT =
84             new HotwordRejectedResult.Builder()
85                     .setConfidenceLevel(HotwordRejectedResult.CONFIDENCE_LEVEL_MEDIUM)
86                     .build();
87     private static final int PIPE_READ_END_INDEX = 0;
88     private static final int PIPE_WRITE_END_INDEX = 1;
89 
90     @NonNull
91     private final Object mLock = new Object();
92     private Handler mHandler;
93     @GuardedBy("mLock")
94     private boolean mStopDetectionCalled;
95     @GuardedBy("mLock")
96     private int mDetectionDelayMs = 0;
97     private long mServiceCreatedTimeMillis = -1;
98 
99     @GuardedBy("mLock")
100     @Nullable
101     private Runnable mDetectionJob;
102 
103     private boolean mIsTestUnexpectedCallback;
104 
105     private boolean mIsTestAudioEgress;
106 
107     /**
108      * It only works when {@link #mIsTestAudioEgress} is true
109      */
110     private boolean mUseIllegalAudioEgressCopyBufferSize;
111 
112     private boolean mIsNoNeedActionDuringDetection;
113 
114     private boolean mCheckAudioDataIsNotZero;
115 
116     private boolean mShouldCloseExternalAudioStreamAfterRead = true;
117 
118     // Only used in certain scenarios where we don't want to close it immediately after reading from
119     // it.
120     private InputStream mExternalAudioStreamReceived;
121     // Only used in certain scenarios where we want to verify whether the stream is writable.
122     private OutputStream mDetectedResultOutputStream;
123 
124     @Override
onCreate()125     public void onCreate() {
126         super.onCreate();
127         mHandler = Handler.createAsync(Looper.getMainLooper());
128         mServiceCreatedTimeMillis = SystemClock.elapsedRealtime();
129         Log.d(TAG, "onCreate");
130     }
131 
132     @Override
onDetect(@onNull AlwaysOnHotwordDetector.EventPayload eventPayload, long timeoutMillis, @NonNull Callback callback)133     public void onDetect(@NonNull AlwaysOnHotwordDetector.EventPayload eventPayload,
134             long timeoutMillis, @NonNull Callback callback) {
135         Log.d(TAG, "onDetect for DSP source");
136 
137         if (mIsNoNeedActionDuringDetection) {
138             mIsNoNeedActionDuringDetection = false;
139             return;
140         }
141 
142         if (!canReadAudio()) {
143             callback.onDetected(DETECTED_RESULT_FOR_MIC_FAILURE);
144             return;
145         }
146 
147         // TODO: Check the capture session (needs to be reflectively accessed).
148         byte[] data = eventPayload.getData();
149         if (data != null && data.length > 0) {
150             if (mIsTestUnexpectedCallback) {
151                 Log.d(TAG, "callback onDetected twice");
152                 callback.onDetected(DETECTED_RESULT);
153                 callback.onDetected(DETECTED_RESULT);
154                 mIsTestUnexpectedCallback = false;
155                 return;
156             }
157 
158             // Create the unaccepted HotwordDetectedResult first to test the protection in the
159             // onDetected callback function of HotwordDetectionService. When the bundle data of
160             // HotwordDetectedResult is larger than max bundle size, it will throw the
161             // IllegalArgumentException.
162             PersistableBundle persistableBundle = new PersistableBundle();
163             HotwordDetectedResult hotwordDetectedResult =
164                     new HotwordDetectedResult.Builder()
165                             .setHotwordPhraseId(eventPayload.getKeyphraseRecognitionExtras().get(
166                                     0).getKeyphraseId())
167                             .setExtras(persistableBundle)
168                             .build();
169             int key = 0;
170             do {
171                 persistableBundle.putInt(Integer.toString(key), 0);
172                 key++;
173             } while (Utils.getParcelableSize(persistableBundle)
174                     <= HotwordDetectedResult.getMaxBundleSize());
175 
176             synchronized (mLock) {
177                 mHandler.postDelayed(() -> {
178                     try {
179                         if (mIsTestAudioEgress) {
180                             if (mUseIllegalAudioEgressCopyBufferSize) {
181                                 callback.onDetected(
182                                         Utils.AUDIO_EGRESS_DETECTED_RESULT_WRONG_COPY_BUFFER_SIZE);
183                             } else {
184                                 callback.onDetected(Utils.AUDIO_EGRESS_DETECTED_RESULT);
185                             }
186                         } else {
187                             callback.onDetected(hotwordDetectedResult);
188                         }
189                     } catch (IllegalArgumentException e) {
190                         callback.onDetected(DETECTED_RESULT);
191                     }
192                 }, mDetectionDelayMs);
193             }
194         } else {
195             callback.onRejected(REJECTED_RESULT);
196             if (mIsTestUnexpectedCallback) {
197                 Log.d(TAG, "callback onRejected again");
198                 callback.onRejected(REJECTED_RESULT);
199                 mIsTestUnexpectedCallback = false;
200             }
201         }
202     }
203 
204     @Override
onDetect( @onNull ParcelFileDescriptor audioStream, @NonNull AudioFormat audioFormat, @Nullable PersistableBundle options, @NonNull Callback callback)205     public void onDetect(
206             @NonNull ParcelFileDescriptor audioStream,
207             @NonNull AudioFormat audioFormat,
208             @Nullable PersistableBundle options,
209             @NonNull Callback callback) {
210         Log.d(TAG, "onDetect for external source");
211 
212         if (callback == null) {
213             Log.w(TAG, "callback is null");
214             return;
215         }
216         if (audioStream == null) {
217             Log.w(TAG, "audioStream is null");
218             return;
219         }
220         if (options != null) {
221             if (options.getBoolean(Utils.KEY_DETECTION_REJECTED, false)) {
222                 Log.d(TAG, "Call onRejected for external source");
223                 callback.onRejected(REJECTED_RESULT);
224                 return;
225             }
226             if (options.getBoolean(Utils.KEY_ACCEPT_DETECTION, false)) {
227                 Log.d(TAG, "Accepting the detection based on options value.");
228                 callback.onDetected(DETECTED_RESULT);
229                 return;
230             }
231         }
232 
233         long startTime = System.currentTimeMillis();
234         InputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(audioStream);
235         try {
236             // We added the fake audio data and set "hotword!" string at the head. Then we simulated
237             // to verify the audio data with "hotword!" in HotwordDetectionService. If the audio
238             // data includes "hotword!", it means that the hotword is valid.
239             while (fis.available() < 8) {
240                 try {
241                     Thread.sleep(10);
242                 } catch (InterruptedException e) {
243                     // Nothing
244                 }
245                 if (System.currentTimeMillis() - startTime > 3000) {
246                     Log.w(TAG, "Over timeout");
247                     return;
248                 }
249             }
250             Log.d(TAG, "fis.available() = " + fis.available());
251             byte[] buffer = new byte[8];
252             fis.read(buffer, 0, 8);
253             if (isSame(buffer, FAKE_HOTWORD_AUDIO_DATA,
254                     buffer.length)) {
255                 Runnable sendDetect = () -> {
256                     Log.d(TAG, "call callback.onDetected");
257                     if (mIsTestAudioEgress) {
258                         if (mUseIllegalAudioEgressCopyBufferSize) {
259                             callback.onDetected(
260                                     Utils.AUDIO_EGRESS_DETECTED_RESULT_WRONG_COPY_BUFFER_SIZE);
261                         } else {
262                             try {
263                                 ParcelFileDescriptor[] audioPipe =
264                                         ParcelFileDescriptor.createPipe();
265                                 OutputStream audioPipeOutputStream =
266                                         new ParcelFileDescriptor.AutoCloseOutputStream(
267                                                 audioPipe[PIPE_WRITE_END_INDEX]);
268                                 audioPipeOutputStream.write(FAKE_HOTWORD_AUDIO_DATA);
269                                 HotwordAudioStream resultAudioStream =
270                                         Utils.createNewHotwordAudioStream(
271                                                 audioPipe[PIPE_READ_END_INDEX]);
272                                 HotwordDetectedResult detectedResult =
273                                         Utils.createNewAudioEgressDetectedResult(
274                                                 resultAudioStream);
275                                 mExternalAudioStreamReceived = fis;
276                                 mDetectedResultOutputStream = audioPipeOutputStream;
277                                 callback.onDetected(detectedResult);
278                                 audioPipe[PIPE_READ_END_INDEX].close();
279                             } catch (IOException ex) {
280                                 Log.e(TAG, "Unexpected IOException: ", ex);
281                             }
282                         }
283                     } else {
284                         callback.onDetected(DETECTED_RESULT);
285                     }
286                 };
287 
288                 synchronized (mLock) {
289                     if (mDetectionDelayMs > 0) {
290                         mHandler.postDelayed(sendDetect, mDetectionDelayMs);
291                     } else {
292                         sendDetect.run();
293                     }
294                 }
295             } else {
296                 callback.onRejected(REJECTED_RESULT);
297             }
298         } catch (IOException e) {
299             Log.w(TAG, "Failed to read data : ", e);
300         } finally {
301             if (mShouldCloseExternalAudioStreamAfterRead) {
302                 Log.d(TAG, "Closing external audio stream.");
303                 try {
304                     fis.close();
305                 } catch (IOException e2) {
306                     Log.e(TAG, "Unable to close external audio stream.", e2);
307                 }
308             }
309         }
310     }
311 
312     @Override
onDetect(@onNull Callback callback)313     public void onDetect(@NonNull Callback callback) {
314         Log.d(TAG, "onDetect for Mic source");
315 
316         synchronized (mLock) {
317             if (mDetectionJob != null) {
318                 throw new IllegalStateException("onDetect called while already detecting");
319             }
320             if (!mStopDetectionCalled) {
321                 // Delaying this allows us to test other flows, such as stopping detection. It's
322                 // also more realistic to schedule it onto another thread.
323                 mDetectionJob = () -> {
324                     Log.d(TAG, "Sending detected result");
325                     if (mIsTestUnexpectedCallback) {
326                         Log.d(TAG, "callback onDetected twice");
327                         callback.onDetected(DETECTED_RESULT);
328                         callback.onDetected(DETECTED_RESULT);
329                         mIsTestUnexpectedCallback = false;
330                         return;
331                     }
332                     if (canReadAudio()) {
333                         if (mIsTestAudioEgress) {
334                             if (mUseIllegalAudioEgressCopyBufferSize) {
335                                 callback.onDetected(
336                                         Utils.AUDIO_EGRESS_DETECTED_RESULT_WRONG_COPY_BUFFER_SIZE);
337                             } else {
338                                 callback.onDetected(Utils.AUDIO_EGRESS_DETECTED_RESULT);
339                             }
340                         } else {
341                             callback.onDetected(DETECTED_RESULT);
342                         }
343                     } else {
344                         callback.onDetected(DETECTED_RESULT_FOR_MIC_FAILURE);
345                     }
346                 };
347                 mHandler.postDelayed(mDetectionJob, mDetectionDelayMs);
348             } else {
349                 Log.d(TAG, "Sending detected result after stop detection");
350                 // We can't store and use this callback in onStopDetection (not valid anymore
351                 // there), so instead we trigger detection again to report the event.
352                 callback.onDetected(DETECTED_RESULT_AFTER_STOP_DETECTION);
353             }
354         }
355     }
356 
357     @Override
onStopDetection()358     public void onStopDetection() {
359         super.onStopDetection();
360         Log.d(TAG, "onStopDetection");
361         synchronized (mLock) {
362             mHandler.removeCallbacks(mDetectionJob);
363             mDetectionJob = null;
364             mStopDetectionCalled = true;
365         }
366     }
367 
368     @Override
onUpdateState( @ullable PersistableBundle options, @Nullable SharedMemory sharedMemory, long callbackTimeoutMillis, @Nullable IntConsumer statusCallback)369     public void onUpdateState(
370             @Nullable PersistableBundle options,
371             @Nullable SharedMemory sharedMemory,
372             long callbackTimeoutMillis,
373             @Nullable IntConsumer statusCallback) {
374         super.onUpdateState(options, sharedMemory, callbackTimeoutMillis, statusCallback);
375         Log.d(TAG, "onUpdateState");
376 
377         // Reset mDetectionJob and mStopDetectionCalled when service is initializing.
378         synchronized (mLock) {
379             // When the service is initializing, the statusCallback will be not null.
380             if (statusCallback != null) {
381                 Log.i(TAG, "onUpdateState called with statusCallback");
382                 if (mDetectionJob != null) {
383                     Log.d(TAG, "onUpdateState mDetectionJob is not null");
384                     mHandler.removeCallbacks(mDetectionJob);
385                     mDetectionJob = null;
386                 }
387                 mStopDetectionCalled = false;
388 
389                 if (options != null) {
390                     if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
391                             == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_SEND_OVER_MAX_INIT_STATUS) {
392                         Log.d(TAG, "send over the max custom initialization status");
393                         final int initializationStatus =
394                                 SandboxedDetectionInitializer.getMaxCustomInitializationStatus();
395                         try {
396                             statusCallback.accept(initializationStatus + 1);
397                         } catch (IllegalArgumentException ex) {
398                             Log.d(TAG, "expect to get IllegalArgumentException here");
399                             statusCallback.accept(initializationStatus);
400                         }
401                         return;
402                     } else if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
403                             == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_SEND_CUSTOM_INIT_STATUS) {
404                         Log.d(TAG, "send custom initialization status");
405                         statusCallback.accept(options.getInt(Utils.KEY_INITIALIZATION_STATUS, -1));
406                         return;
407                     } else if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
408                             == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_SEND_SUCCESS_IF_CREATED_AFTER) {
409                         // verify that the HotwordDetectionService was created after the timestamp
410                         // passed in via the options parameter
411                         long optionsTimestampMillis = options.getLong(Utils.KEY_TIMESTAMP_MILLIS,
412                                 -1);
413                         Log.d(TAG, "send initialization success if created after: "
414                                 + optionsTimestampMillis
415                                 + ", onCreate timestamp=" + mServiceCreatedTimeMillis);
416                         statusCallback.accept((mServiceCreatedTimeMillis > optionsTimestampMillis)
417                                 ? SandboxedDetectionInitializer.INITIALIZATION_STATUS_SUCCESS
418                                 : SandboxedDetectionInitializer.getMaxCustomInitializationStatus());
419                         return;
420                     }
421                 }
422             }
423             if (options != null) {
424                 mDetectionDelayMs = options.getInt(Utils.KEY_DETECTION_DELAY_MS, 0);
425 
426                 if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
427                         == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_CLEAR_SOFTWARE_DETECTION_JOB) {
428                     Log.d(TAG, "options : Clear software detection job");
429                     if (mDetectionJob != null) {
430                         Log.d(TAG, "Clear mDetectionJob");
431                         mHandler.removeCallbacks(mDetectionJob);
432                         mDetectionJob = null;
433                     }
434                     return;
435                 }
436             }
437         }
438 
439         if (options != null) {
440             if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
441                     == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_CRASH) {
442                 Log.d(TAG, "Crash itself. Pid: " + Process.myPid());
443                 Process.killProcess(Process.myPid());
444                 return;
445             }
446             if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
447                     == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ON_UPDATE_STATE_UNEXPECTED_CALLBACK) {
448                 Log.d(TAG, "options : Test unexpected callback");
449                 mIsTestUnexpectedCallback = true;
450                 return;
451             }
452             if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
453                     == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_ENABLE_AUDIO_EGRESS) {
454                 mUseIllegalAudioEgressCopyBufferSize = options.getBoolean(
455                         Utils.KEY_AUDIO_EGRESS_USE_ILLEGAL_COPY_BUFFER_SIZE,
456                         /* defaultValue= */ false);
457                 mShouldCloseExternalAudioStreamAfterRead =
458                         options.getBoolean(
459                                 Utils.KEY_AUDIO_EGRESS_CLOSE_AUDIO_STREAM_AFTER_READ,
460                                 /* defaultValue= */ true);
461                 Log.d(TAG, "options : Test audio egress use illegal copy buffer size = "
462                         + mUseIllegalAudioEgressCopyBufferSize);
463                 mIsTestAudioEgress = true;
464                 return;
465             }
466             if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
467                     == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_NO_NEED_ACTION_DURING_DETECTION) {
468                 Log.d(TAG, "options : Test no need action during detection");
469                 mIsNoNeedActionDuringDetection = true;
470                 return;
471             }
472             if (options.getInt(Utils.KEY_TEST_SCENARIO, -1)
473                     == Utils.EXTRA_HOTWORD_DETECTION_SERVICE_CAN_READ_AUDIO_DATA_IS_NOT_ZERO) {
474                 Log.d(TAG, "options : Test can read audio, and data is not zero");
475                 mCheckAudioDataIsNotZero = true;
476                 return;
477             }
478             if (options.getBoolean(
479                     Utils.KEY_CLOSE_INPUT_AUDIO_STREAM_IF_OUTPUT_PIPE_BROKEN, false)) {
480                 Log.d(TAG, "Received request to close input audio stream if output is closed");
481                 if (mExternalAudioStreamReceived == null) {
482                     Log.e(TAG, "Ignored request because input stream is null");
483                     return;
484                 }
485                 if (mDetectedResultOutputStream == null) {
486                     Log.e(TAG, "Ignored request because result output stream is null");
487                     return;
488                 }
489                 try {
490                     /*
491                      * Write an arbitrary byte. This should trigger an error in the system_server
492                      * thread that copies from the result this service returns to the result
493                      * received by the test.
494                      */
495                     mDetectedResultOutputStream.write(1);
496                     /*
497                      * Wait for the copy thread mentioned above to clean up, which should close the
498                      * read-end of mDetectedResultOutputStream. Then write one more byte to trigger
499                      * an IOException here.
500                      */
501                     SystemClock.sleep(2000);
502                     mDetectedResultOutputStream.write(1);
503 
504                 } catch (IOException ex) {
505                     Log.d(
506                             TAG,
507                             "Output stream unwritable (which is expected), closing external audio"
508                                     + " stream received from wearable.");
509                     try {
510                         mExternalAudioStreamReceived.close();
511                     } catch (IOException ex2) {
512                         Log.e(
513                                 TAG,
514                                 "Unable to close external audio stream received from wearable.",
515                                 ex2);
516                     }
517                 }
518                 return;
519             }
520 
521             String fakeData = options.getString(KEY_FAKE_DATA);
522             if (!TextUtils.equals(fakeData, VALUE_FAKE_DATA)) {
523                 Log.d(TAG, "options : data is not the same");
524                 return;
525             }
526         }
527 
528         if (sharedMemory != null) {
529             try {
530                 sharedMemory.mapReadWrite();
531                 Log.d(TAG, "sharedMemory : is not read-only");
532                 return;
533             } catch (ErrnoException e) {
534                 // For read-only case
535             } finally {
536                 sharedMemory.close();
537             }
538         }
539 
540         // Report success
541         Log.d(TAG, "onUpdateState success");
542         if (statusCallback != null) {
543             statusCallback.accept(INITIALIZATION_STATUS_SUCCESS);
544         }
545     }
546 
isSame(byte[] array1, byte[] array2, int length)547     private boolean isSame(byte[] array1, byte[] array2, int length) {
548         if (length <= 0) {
549             return false;
550         }
551         if (array1 == null || array2 == null || array1.length < length || array2.length < length) {
552             return false;
553         }
554         for (int i = 0; i < length; i++) {
555             if (array1[i] != array2[i]) {
556                 return false;
557             }
558         }
559         return true;
560     }
561 
canReadAudio()562     private boolean canReadAudio() {
563         int bytesPerSample = 2; // for ENCODING_PCM_16BIT
564         int sampleRate = 16000;
565         int bytesPerSecond = bytesPerSample * sampleRate; // for single channel
566         AudioRecord record =
567                 new AudioRecord.Builder()
568                         .setAudioAttributes(
569                                 new AudioAttributes.Builder()
570                                         .setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD)
571                                         .build())
572                         .setAudioFormat(
573                                 new AudioFormat.Builder()
574                                         .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
575                                         .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
576                                         .setSampleRate(sampleRate)
577                                         .build())
578                         .setBufferSizeInBytes(bytesPerSecond)
579                         .build();
580         if (record.getState() != AudioRecord.STATE_INITIALIZED) {
581             Log.e(TAG, "Failed to initialize AudioRecord");
582             record.release();
583             return false;
584         }
585 
586         record.startRecording();
587         try {
588             byte[] buffer = new byte[bytesPerSecond]; // read 1 second of audio
589             int numBytes = 0;
590             while (numBytes < buffer.length) {
591                 int bytesRead =
592                         record.read(buffer, numBytes, Math.min(1024, buffer.length - numBytes));
593                 if (bytesRead < 0) {
594                     Log.e(TAG, "Error reading from mic: " + bytesRead);
595                     return false;
596                 }
597                 numBytes += bytesRead;
598             }
599             // The audio data will be zero on virtual device, so it would be better to skip to
600             // check the audio data.
601             if (Utils.isVirtualDevice()) {
602                 return true;
603             }
604             if (mCheckAudioDataIsNotZero) {
605                 for (byte b : buffer) {
606                     // TODO: Maybe check that some portion of the bytes are non-zero.
607                     if (b != 0) {
608                         return true;
609                     }
610                 }
611                 Log.d(TAG, "All data are zero");
612                 return false;
613             }
614 
615             return true;
616         } finally {
617             record.release();
618         }
619     }
620 }
621