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