1 /* 2 * Copyright (C) 2019 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 package com.android.car.bugreport; 17 18 import static com.android.car.bugreport.BugReportService.MAX_PROGRESS_VALUE; 19 20 import static java.util.stream.Collectors.collectingAndThen; 21 import static java.util.stream.Collectors.toList; 22 23 import android.Manifest; 24 import android.app.Activity; 25 import android.car.Car; 26 import android.car.CarNotConnectedException; 27 import android.car.drivingstate.CarDrivingStateEvent; 28 import android.car.drivingstate.CarDrivingStateManager; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.ServiceConnection; 33 import android.content.pm.PackageManager; 34 import android.media.AudioAttributes; 35 import android.media.AudioFocusRequest; 36 import android.media.AudioManager; 37 import android.media.MediaCodecInfo; 38 import android.media.MediaCodecList; 39 import android.media.MediaFormat; 40 import android.media.MediaRecorder; 41 import android.os.AsyncTask; 42 import android.os.Bundle; 43 import android.os.CountDownTimer; 44 import android.os.Handler; 45 import android.os.IBinder; 46 import android.os.Looper; 47 import android.os.UserManager; 48 import android.util.Log; 49 import android.view.View; 50 import android.view.Window; 51 import android.widget.Button; 52 import android.widget.ProgressBar; 53 import android.widget.TextView; 54 import android.widget.Toast; 55 56 import com.google.common.base.Preconditions; 57 import com.google.common.base.Strings; 58 import com.google.common.collect.ImmutableList; 59 import com.google.common.collect.ImmutableSortedSet; 60 import com.google.common.io.ByteStreams; 61 62 import java.io.File; 63 import java.io.FileInputStream; 64 import java.io.IOException; 65 import java.io.InputStream; 66 import java.io.OutputStream; 67 import java.util.Date; 68 import java.util.Locale; 69 import java.util.Objects; 70 import java.util.Random; 71 72 /** 73 * Activity that shows two types of dialogs: starting a new bug report and current status of already 74 * in progress bug report. 75 * 76 * <p>If there is no in-progress bug report, it starts recording voice message. After clicking 77 * submit button it initiates {@link BugReportService}. 78 * 79 * <p>If bug report is in-progress, it shows a progress bar. 80 */ 81 public class BugReportActivity extends Activity { 82 private static final String TAG = BugReportActivity.class.getSimpleName(); 83 84 /** Starts {@link MetaBugReport#TYPE_AUDIO_FIRST} bugreporting. */ 85 private static final String ACTION_START_AUDIO_FIRST = 86 "com.android.car.bugreport.action.START_AUDIO_FIRST"; 87 88 /** This is used internally by {@link BugReportService}. */ 89 private static final String ACTION_ADD_AUDIO = 90 "com.android.car.bugreport.action.ADD_AUDIO"; 91 92 private static final int VOICE_MESSAGE_MAX_DURATION_MILLIS = 60 * 1000; 93 private static final int PERMISSIONS_REQUEST_ID = 1; 94 private static final ImmutableSortedSet<String> REQUIRED_PERMISSIONS = ImmutableSortedSet.of( 95 Manifest.permission.RECORD_AUDIO, Manifest.permission.POST_NOTIFICATIONS); 96 97 private static final String EXTRA_BUGREPORT_ID = "bugreport-id"; 98 99 private static final String AUDIO_FILE_EXTENSION_WAV = "wav"; 100 private static final String AUDIO_FILE_EXTENSION_3GPP = "3gp"; 101 102 /** 103 * NOTE: mRecorder related messages are cleared when the activity finishes. 104 */ 105 private final Handler mHandler = new Handler(Looper.getMainLooper()); 106 107 private final String mAudioFormat = isCodecSupported(MediaFormat.MIMETYPE_AUDIO_AMR_WB) 108 ? MediaFormat.MIMETYPE_AUDIO_AMR_WB : MediaFormat.MIMETYPE_AUDIO_AAC; 109 110 /** Look up string length, e.g. [ABCDEF]. */ 111 static final int LOOKUP_STRING_LENGTH = 6; 112 113 private TextView mInProgressTitleText; 114 private ProgressBar mProgressBar; 115 private TextView mProgressText; 116 private TextView mAddAudioText; 117 private TextView mTimerText; 118 private VoiceRecordingView mVoiceRecordingView; 119 private View mVoiceRecordingFinishedView; 120 private View mSubmitBugReportLayout; 121 private View mInProgressLayout; 122 private View mShowBugReportsButton; 123 private Button mSubmitButton; 124 125 private boolean mBound; 126 /** Audio message recording process started (including waiting for permission). */ 127 private boolean mAudioRecordingStarted; 128 /** Audio recording using MIC is running (permission given). */ 129 private boolean mAudioRecordingIsRunning; 130 private boolean mIsNewBugReport; 131 private boolean mIsOnActivityStartedWithBugReportServiceBoundCalled; 132 private boolean mIsSubmitButtonClicked; 133 private BugReportService mService; 134 private MediaRecorder mRecorder; 135 private MetaBugReport mMetaBugReport; 136 private File mTempAudioFile; 137 private Car mCar; 138 private CarDrivingStateManager mDrivingStateManager; 139 private AudioManager mAudioManager; 140 private AudioFocusRequest mLastAudioFocusRequest; 141 private Config mConfig; 142 private CountDownTimer mCountDownTimer; 143 144 /** Defines callbacks for service binding, passed to bindService() */ 145 private ServiceConnection mConnection = new ServiceConnection() { 146 @Override 147 public void onServiceConnected(ComponentName className, IBinder service) { 148 BugReportService.ServiceBinder binder = (BugReportService.ServiceBinder) service; 149 mService = binder.getService(); 150 mBound = true; 151 onActivityStartedWithBugReportServiceBound(); 152 } 153 154 @Override 155 public void onServiceDisconnected(ComponentName arg0) { 156 // called when service connection breaks unexpectedly. 157 mBound = false; 158 } 159 }; 160 161 /** 162 * Builds an intent that starts {@link BugReportActivity} to add audio message to the existing 163 * bug report. 164 */ buildAddAudioIntent(Context context, int bugReportId)165 static Intent buildAddAudioIntent(Context context, int bugReportId) { 166 Intent addAudioIntent = new Intent(context, BugReportActivity.class); 167 addAudioIntent.setAction(ACTION_ADD_AUDIO); 168 addAudioIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 169 addAudioIntent.putExtra(EXTRA_BUGREPORT_ID, bugReportId); 170 return addAudioIntent; 171 } 172 buildStartBugReportIntent(Context context)173 static Intent buildStartBugReportIntent(Context context) { 174 Intent intent = new Intent(context, BugReportActivity.class); 175 intent.setAction(ACTION_START_AUDIO_FIRST); 176 // Clearing is needed, otherwise multiple BugReportActivity-ies get opened and 177 // MediaRecorder crashes. 178 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 179 return intent; 180 } 181 182 @Override onCreate(Bundle savedInstanceState)183 public void onCreate(Bundle savedInstanceState) { 184 Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled."); 185 186 super.onCreate(savedInstanceState); 187 requestWindowFeature(Window.FEATURE_NO_TITLE); 188 189 // Bind to BugReportService. 190 Intent intent = new Intent(this, BugReportService.class); 191 bindService(intent, mConnection, BIND_AUTO_CREATE); 192 } 193 194 @Override onStart()195 protected void onStart() { 196 super.onStart(); 197 198 if (mBound) { 199 onActivityStartedWithBugReportServiceBound(); 200 } 201 } 202 203 @Override onStop()204 protected void onStop() { 205 super.onStop(); 206 // If SUBMIT button is clicked, cancelling audio has been taken care of. 207 if (!mIsSubmitButtonClicked) { 208 cancelAudioMessageRecording(); 209 } 210 if (mBound) { 211 mService.removeBugReportProgressListener(); 212 } 213 // Reset variables for the next onStart(). 214 mAudioRecordingStarted = false; 215 mAudioRecordingIsRunning = false; 216 mIsSubmitButtonClicked = false; 217 mIsOnActivityStartedWithBugReportServiceBoundCalled = false; 218 mMetaBugReport = null; 219 mTempAudioFile = null; 220 } 221 222 @Override onDestroy()223 public void onDestroy() { 224 if (mBound) { 225 unbindService(mConnection); 226 mBound = false; 227 } 228 if (mCar != null && mCar.isConnected()) { 229 mCar.disconnect(); 230 mCar = null; 231 } 232 super.onDestroy(); 233 } 234 onCarDrivingStateChanged(CarDrivingStateEvent event)235 private void onCarDrivingStateChanged(CarDrivingStateEvent event) { 236 if (mShowBugReportsButton == null) { 237 Log.w(TAG, "Cannot handle driving state change, UI is not ready"); 238 return; 239 } 240 // When adding audio message to the existing bugreport, do not show "Show Bug Reports" 241 // button, users either should explicitly Submit or Cancel. 242 if (mAudioRecordingStarted && !mIsNewBugReport) { 243 mShowBugReportsButton.setVisibility(View.GONE); 244 return; 245 } 246 if (event.eventValue == CarDrivingStateEvent.DRIVING_STATE_PARKED 247 || event.eventValue == CarDrivingStateEvent.DRIVING_STATE_IDLING) { 248 mShowBugReportsButton.setVisibility(View.VISIBLE); 249 } else { 250 mShowBugReportsButton.setVisibility(View.GONE); 251 } 252 } 253 onProgressChanged(float progress)254 private void onProgressChanged(float progress) { 255 int progressValue = (int) progress; 256 mProgressBar.setProgress(progressValue); 257 mProgressText.setText(progressValue + "%"); 258 if (progressValue == MAX_PROGRESS_VALUE) { 259 mInProgressTitleText.setText(R.string.bugreport_dialog_in_progress_title_finished); 260 } 261 } 262 prepareUi()263 private void prepareUi() { 264 if (mSubmitBugReportLayout != null) { 265 return; 266 } 267 setContentView(R.layout.bug_report_activity); 268 269 // Connect to the services here, because they are used only when showing the dialog. 270 // We need to minimize system state change when performing TYPE_AUDIO_LATER bug report. 271 mConfig = Config.create(); 272 mCar = Car.createCar(this, /* handler= */ null, 273 Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT, this::onCarLifecycleChanged); 274 275 mInProgressTitleText = findViewById(R.id.in_progress_title_text); 276 mProgressBar = findViewById(R.id.progress_bar); 277 mProgressText = findViewById(R.id.progress_text); 278 mAddAudioText = findViewById(R.id.bug_report_add_audio_to_existing); 279 mVoiceRecordingView = findViewById(R.id.voice_recording_view); 280 mTimerText = findViewById(R.id.voice_recording_timer_text_view); 281 mVoiceRecordingFinishedView = findViewById(R.id.voice_recording_finished_text_view); 282 mSubmitBugReportLayout = findViewById(R.id.submit_bug_report_layout); 283 mInProgressLayout = findViewById(R.id.in_progress_layout); 284 mShowBugReportsButton = findViewById(R.id.button_show_bugreports); 285 mSubmitButton = findViewById(R.id.button_submit); 286 287 mShowBugReportsButton.setOnClickListener(this::buttonShowBugReportsClick); 288 mSubmitButton.setOnClickListener(this::buttonSubmitClick); 289 findViewById(R.id.button_cancel).setOnClickListener(this::buttonCancelClick); 290 findViewById(R.id.button_close).setOnClickListener(this::buttonCancelClick); 291 findViewById(R.id.button_record_again).setOnClickListener(this::buttonRecordAgainClick); 292 293 if (mIsNewBugReport) { 294 mSubmitButton.setText(R.string.bugreport_dialog_submit); 295 } else { 296 mSubmitButton.setText(mConfig.isAutoUpload() 297 ? R.string.bugreport_dialog_upload : R.string.bugreport_dialog_save); 298 } 299 } 300 onCarLifecycleChanged(Car car, boolean ready)301 private void onCarLifecycleChanged(Car car, boolean ready) { 302 if (!ready) { 303 mDrivingStateManager = null; 304 mCar = null; 305 Log.d(TAG, "Car service is not ready, ignoring"); 306 // If car service is not ready for this activity, just ignore it - as it's only 307 // used to control UX restrictions. 308 return; 309 } 310 try { 311 mDrivingStateManager = (CarDrivingStateManager) car.getCarManager( 312 Car.CAR_DRIVING_STATE_SERVICE); 313 mDrivingStateManager.registerListener( 314 BugReportActivity.this::onCarDrivingStateChanged); 315 // Call onCarDrivingStateChanged(), because it's not called when Car is connected. 316 onCarDrivingStateChanged(mDrivingStateManager.getCurrentCarDrivingState()); 317 } catch (CarNotConnectedException e) { 318 Log.w(TAG, "Failed to get CarDrivingStateManager", e); 319 } 320 } 321 showInProgressUi()322 private void showInProgressUi() { 323 mSubmitBugReportLayout.setVisibility(View.GONE); 324 mInProgressLayout.setVisibility(View.VISIBLE); 325 mInProgressTitleText.setText(R.string.bugreport_dialog_in_progress_title); 326 onProgressChanged(mService.getBugReportProgress()); 327 } 328 showSubmitBugReportUi(boolean isRecording)329 private void showSubmitBugReportUi(boolean isRecording) { 330 mSubmitBugReportLayout.setVisibility(View.VISIBLE); 331 mInProgressLayout.setVisibility(View.GONE); 332 if (isRecording) { 333 mVoiceRecordingFinishedView.setVisibility(View.GONE); 334 mVoiceRecordingView.setVisibility(View.VISIBLE); 335 mTimerText.setVisibility(View.VISIBLE); 336 } else { 337 mVoiceRecordingFinishedView.setVisibility(View.VISIBLE); 338 mVoiceRecordingView.setVisibility(View.GONE); 339 mTimerText.setVisibility(View.GONE); 340 } 341 // NOTE: mShowBugReportsButton visibility is also handled in #onCarDrivingStateChanged(). 342 mShowBugReportsButton.setVisibility(View.GONE); 343 if (mDrivingStateManager != null) { 344 try { 345 onCarDrivingStateChanged(mDrivingStateManager.getCurrentCarDrivingState()); 346 } catch (CarNotConnectedException e) { 347 Log.e(TAG, "Failed to get current driving state.", e); 348 } 349 } 350 } 351 352 /** 353 * Initializes MetaBugReport in a local DB and starts audio recording. 354 * 355 * <p>This method expected to be called when the activity is started and bound to the service. 356 */ onActivityStartedWithBugReportServiceBound()357 private void onActivityStartedWithBugReportServiceBound() { 358 if (mIsOnActivityStartedWithBugReportServiceBoundCalled) { 359 return; 360 } 361 mIsOnActivityStartedWithBugReportServiceBoundCalled = true; 362 363 if (mService.isCollectingBugReport()) { 364 Log.i(TAG, "Bug report is already being collected."); 365 mService.setBugReportProgressListener(this::onProgressChanged); 366 prepareUi(); 367 showInProgressUi(); 368 return; 369 } 370 371 if (ACTION_START_AUDIO_FIRST.equals(getIntent().getAction())) { 372 Log.i(TAG, "Starting a TYPE_AUDIO_FIRST bugreport."); 373 createNewBugReportWithAudioMessage(); 374 } else if (ACTION_ADD_AUDIO.equals(getIntent().getAction())) { 375 addAudioToExistingBugReport( 376 getIntent().getIntExtra(EXTRA_BUGREPORT_ID, /* defaultValue= */ -1)); 377 } else { 378 Log.w(TAG, "Unsupported intent action provided: " + getIntent().getAction()); 379 finish(); 380 } 381 } 382 getAudioFileExtension()383 private String getAudioFileExtension() { 384 if (mAudioFormat.equals(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) { 385 return AUDIO_FILE_EXTENSION_WAV; 386 } 387 return AUDIO_FILE_EXTENSION_3GPP; 388 } 389 addAudioToExistingBugReport(int existingBugReportId)390 private void addAudioToExistingBugReport(int existingBugReportId) { 391 MetaBugReport existingBugReport = BugStorageUtils.findBugReport(this, 392 existingBugReportId).orElseThrow(() -> new RuntimeException( 393 "Failed to find bug report with id " + existingBugReportId)); 394 Log.i(TAG, "Adding audio to the existing bugreport " + existingBugReport.getTimestamp()); 395 startAudioMessageRecording(/* isNewBugReport= */ false, existingBugReport, 396 createTempAudioFileInCacheDirectory()); 397 } 398 createNewBugReportWithAudioMessage()399 private void createNewBugReportWithAudioMessage() { 400 MetaBugReport newBugReport = createBugReport(this, MetaBugReport.TYPE_AUDIO_FIRST); 401 startAudioMessageRecording(/* isNewBugReport= */ true, newBugReport, 402 createTempAudioFileInCacheDirectory()); 403 } 404 405 /** 406 * Creates a temporary audio file in cache directory for voice recording. 407 * 408 * For example, /data/user/10/com.android.car.bugreport/cache/audio1128264677920904030.wav 409 */ createTempAudioFileInCacheDirectory()410 private File createTempAudioFileInCacheDirectory() { 411 try { 412 return File.createTempFile("audio", "." + getAudioFileExtension(), 413 getCacheDir()); 414 } catch (IOException e) { 415 throw new RuntimeException("failed to create temp audio file", e); 416 } 417 } 418 419 /** Shows a dialog UI and starts recording audio message. */ startAudioMessageRecording( boolean isNewBugReport, MetaBugReport bugReport, File tempAudioFile)420 private void startAudioMessageRecording( 421 boolean isNewBugReport, MetaBugReport bugReport, File tempAudioFile) { 422 if (mAudioRecordingStarted) { 423 Log.i(TAG, "Audio message recording is already started."); 424 return; 425 } 426 mAudioRecordingStarted = true; 427 428 // Close the notification shade and other dialogs when showing the audio record dialog. 429 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 430 431 mAudioManager = getSystemService(AudioManager.class); 432 mIsNewBugReport = isNewBugReport; 433 mMetaBugReport = bugReport; 434 mTempAudioFile = tempAudioFile; 435 prepareUi(); 436 showSubmitBugReportUi(/* isRecording= */ true); 437 if (isNewBugReport) { 438 mAddAudioText.setVisibility(View.GONE); 439 } else { 440 mAddAudioText.setVisibility(View.VISIBLE); 441 mAddAudioText.setText(String.format( 442 getString(R.string.bugreport_dialog_add_audio_to_existing), 443 mMetaBugReport.getTimestamp())); 444 } 445 446 ImmutableList<String> missingPermissions = findMissingPermissions(); 447 if (missingPermissions.isEmpty()) { 448 startRecordingWithPermission(); 449 } else { 450 requestPermissions(missingPermissions.toArray(new String[missingPermissions.size()]), 451 PERMISSIONS_REQUEST_ID); 452 } 453 } 454 455 /** 456 * Finds required permissions not granted. 457 */ findMissingPermissions()458 private ImmutableList<String> findMissingPermissions() { 459 return REQUIRED_PERMISSIONS.stream().filter(permission -> checkSelfPermission(permission) 460 != PackageManager.PERMISSION_GRANTED).collect( 461 collectingAndThen(toList(), ImmutableList::copyOf)); 462 } 463 464 /** 465 * Cancels bugreporting by stopping audio recording and deleting temp audio file. 466 */ cancelAudioMessageRecording()467 private void cancelAudioMessageRecording() { 468 // If audio recording is not running, most likely there were permission issues, 469 // so leave the bugreport as is without cancelling it. 470 if (!mAudioRecordingIsRunning) { 471 Log.w(TAG, "Cannot cancel, audio recording is not running."); 472 return; 473 } 474 stopAudioRecording(); 475 if (mIsNewBugReport) { 476 BugStorageUtils.setBugReportStatus( 477 this, mMetaBugReport, Status.STATUS_USER_CANCELLED, ""); 478 Log.i(TAG, "Bug report " + mMetaBugReport.getTimestamp() + " is cancelled"); 479 } 480 new DeleteFilesAndDirectoriesAsyncTask().execute(mTempAudioFile); 481 mAudioRecordingStarted = false; 482 mAudioRecordingIsRunning = false; 483 } 484 buttonCancelClick(View view)485 private void buttonCancelClick(View view) { 486 finish(); 487 } 488 buttonRecordAgainClick(View view)489 private void buttonRecordAgainClick(View view) { 490 stopAudioRecording(); 491 showSubmitBugReportUi(/* isRecording= */ true); 492 startRecordingWithPermission(); 493 } 494 buttonSubmitClick(View view)495 private void buttonSubmitClick(View view) { 496 stopAudioRecording(); 497 mIsSubmitButtonClicked = true; 498 499 new AddAudioToBugReportAsyncTask(this, mConfig, mMetaBugReport, mTempAudioFile, 500 mIsNewBugReport).execute(); 501 502 setResult(Activity.RESULT_OK); 503 finish(); 504 } 505 506 507 /** 508 * Starts {@link BugReportInfoActivity} and finishes current activity, so it won't be running 509 * in the background and closing {@link BugReportInfoActivity} will not open the current 510 * activity again. 511 */ buttonShowBugReportsClick(View view)512 private void buttonShowBugReportsClick(View view) { 513 // First cancel the audio recording, then delete the bug report from database. 514 cancelAudioMessageRecording(); 515 // Delete the bugreport from database, otherwise pressing "Show Bugreports" button will 516 // create unnecessary cancelled bugreports. 517 if (mMetaBugReport != null) { 518 BugStorageUtils.completeDeleteBugReport(this, mMetaBugReport.getId()); 519 } 520 Intent intent = new Intent(this, BugReportInfoActivity.class); 521 startActivity(intent); 522 finish(); 523 } 524 525 @Override onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults)526 public void onRequestPermissionsResult( 527 int requestCode, String[] permissions, int[] grantResults) { 528 if (requestCode != PERMISSIONS_REQUEST_ID) { 529 return; 530 } 531 532 ImmutableList<String> missingPermissions = findMissingPermissions(); 533 if (missingPermissions.isEmpty()) { 534 // Start recording from UI thread, otherwise when MediaRecord#start() fails, 535 // stack trace gets confusing. 536 mHandler.post(this::startRecordingWithPermission); 537 } else { 538 handleMissingPermissions(missingPermissions); 539 } 540 } 541 handleMissingPermissions(ImmutableList missingPermissions)542 private void handleMissingPermissions(ImmutableList missingPermissions) { 543 String text = this.getText(R.string.toast_permissions_denied) + " : " 544 + String.join(", ", missingPermissions); 545 Log.w(TAG, text); 546 Toast.makeText(this, text, Toast.LENGTH_LONG).show(); 547 if (mMetaBugReport == null) { 548 finish(); 549 return; 550 } 551 if (mIsNewBugReport) { 552 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, 553 Status.STATUS_USER_CANCELLED, text); 554 } else { 555 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, 556 Status.STATUS_AUDIO_PENDING, text); 557 } 558 finish(); 559 } 560 isCodecSupported(String codec)561 private boolean isCodecSupported(String codec) { 562 MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 563 for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { 564 if (!codecInfo.isEncoder()) { 565 continue; 566 } 567 for (String mimeType : codecInfo.getSupportedTypes()) { 568 if (mimeType.equalsIgnoreCase(codec)) { 569 return true; 570 } 571 } 572 } 573 return false; 574 } 575 createMediaRecorder()576 private MediaRecorder createMediaRecorder() { 577 MediaRecorder mediaRecorder = new MediaRecorder(); 578 mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); 579 if (mAudioFormat.equals(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) { 580 Log.i(TAG, "Audio encoding is selected to AMR_WB"); 581 mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_WB); 582 mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_WB); 583 } else { 584 Log.i(TAG, "Audio encoding is selected to AAC"); 585 mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); 586 mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); 587 } 588 mediaRecorder.setAudioSamplingRate(16000); 589 mediaRecorder.setOnInfoListener((MediaRecorder recorder, int what, int extra) -> 590 Log.i(TAG, "OnMediaRecorderInfo: what=" + what + ", extra=" + extra)); 591 mediaRecorder.setOnErrorListener((MediaRecorder recorder, int what, int extra) -> 592 Log.i(TAG, "OnMediaRecorderError: what=" + what + ", extra=" + extra)); 593 mediaRecorder.setOutputFile(mTempAudioFile); 594 return mediaRecorder; 595 } 596 startRecordingWithPermission()597 private void startRecordingWithPermission() { 598 Log.i(TAG, "Started voice recording, and saving audio to " + mTempAudioFile); 599 600 mLastAudioFocusRequest = new AudioFocusRequest.Builder( 601 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) 602 .setOnAudioFocusChangeListener(focusChange -> 603 Log.d(TAG, "AudioManager focus change " + focusChange)) 604 .setAudioAttributes(new AudioAttributes.Builder() 605 .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) 606 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) 607 .build()) 608 .setAcceptsDelayedFocusGain(true) 609 .build(); 610 int focusGranted = Objects.requireNonNull(mAudioManager) 611 .requestAudioFocus(mLastAudioFocusRequest); 612 // NOTE: We will record even if the audio focus was not granted. 613 Log.d(TAG, 614 "AudioFocus granted " + (focusGranted == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)); 615 616 mRecorder = createMediaRecorder(); 617 618 try { 619 mRecorder.prepare(); 620 } catch (IOException e) { 621 Log.e(TAG, "Failed on MediaRecorder#prepare(), filename: " + mTempAudioFile, e); 622 finish(); 623 return; 624 } 625 626 mCountDownTimer = createCountDownTimer(); 627 mCountDownTimer.start(); 628 629 mRecorder.start(); 630 mVoiceRecordingView.setRecorder(mRecorder); 631 mAudioRecordingIsRunning = true; 632 } 633 createCountDownTimer()634 private CountDownTimer createCountDownTimer() { 635 return new CountDownTimer(VOICE_MESSAGE_MAX_DURATION_MILLIS, 1000) { 636 public void onTick(long millisUntilFinished) { 637 long secondsRemaining = millisUntilFinished / 1000; 638 String secondText = secondsRemaining > 1 ? "seconds" : "second"; 639 mTimerText.setText(String.format(Locale.US, "%d %s remaining", secondsRemaining, 640 secondText)); 641 } 642 643 public void onFinish() { 644 Log.i(TAG, "Timed out while recording voice message."); 645 stopAudioRecording(); 646 showSubmitBugReportUi(/* isRecording= */ false); 647 } 648 }; 649 } 650 651 private void stopAudioRecording() { 652 mCountDownTimer.cancel(); 653 if (mRecorder != null) { 654 Log.i(TAG, "Recording ended, stopping the MediaRecorder."); 655 try { 656 mRecorder.stop(); 657 } catch (RuntimeException e) { 658 // Sometimes MediaRecorder doesn't start and stopping it throws an error. 659 // We just log these cases, no need to crash the app. 660 Log.w(TAG, "Couldn't stop media recorder", e); 661 } 662 mRecorder.release(); 663 mRecorder = null; 664 } 665 if (mLastAudioFocusRequest != null) { 666 int focusAbandoned = Objects.requireNonNull(mAudioManager) 667 .abandonAudioFocusRequest(mLastAudioFocusRequest); 668 Log.d(TAG, "Audio focus abandoned " 669 + (focusAbandoned == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)); 670 mLastAudioFocusRequest = null; 671 } 672 mVoiceRecordingView.setRecorder(null); 673 } 674 675 private static String getCurrentUserName(Context context) { 676 UserManager um = UserManager.get(context); 677 return um.getUserName(); 678 } 679 680 /** 681 * Creates a {@link MetaBugReport} and saves it in a local sqlite database. 682 * 683 * @param context an Android context. 684 * @param type bug report type, {@link MetaBugReport.BugReportType}. 685 */ 686 static MetaBugReport createBugReport(Context context, int type) { 687 String timestamp = MetaBugReport.toBugReportTimestamp(new Date()); 688 String username = getCurrentUserName(context); 689 String title = BugReportTitleGenerator.generateBugReportTitle(timestamp, username); 690 return BugStorageUtils.createBugReport(context, title, timestamp, username, type); 691 } 692 693 /** A helper class to generate bugreport title. */ 694 private static final class BugReportTitleGenerator { 695 /** Contains easily readable characters. */ 696 private static final char[] CHARS_FOR_RANDOM_GENERATOR = 697 new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 698 'R', 'S', 'T', 'U', 'W', 'X', 'Y', 'Z'}; 699 700 /** 701 * Generates a bugreport title from given timestamp and username. 702 * 703 * <p>Example: "[A45E8] Feedback from user Driver at 2019-09-21_12:00:00" 704 */ 705 static String generateBugReportTitle(String timestamp, String username) { 706 // Lookup string is used to search a bug in Buganizer (see b/130915969). 707 String lookupString = generateRandomString(LOOKUP_STRING_LENGTH); 708 return "[" + lookupString + "] Feedback from user " + username + " at " + timestamp; 709 } 710 711 private static String generateRandomString(int length) { 712 Random random = new Random(); 713 StringBuilder builder = new StringBuilder(); 714 for (int i = 0; i < length; i++) { 715 int randomIndex = random.nextInt(CHARS_FOR_RANDOM_GENERATOR.length); 716 builder.append(CHARS_FOR_RANDOM_GENERATOR[randomIndex]); 717 } 718 return builder.toString(); 719 } 720 } 721 722 /** AsyncTask that recursively deletes files and directories. */ 723 private static class DeleteFilesAndDirectoriesAsyncTask extends AsyncTask<File, Void, Void> { 724 @Override 725 protected Void doInBackground(File... files) { 726 for (File file : files) { 727 Log.i(TAG, "Deleting " + file.getAbsolutePath()); 728 if (file.isFile()) { 729 file.delete(); 730 } else { 731 FileUtils.deleteDirectory(file); 732 } 733 } 734 return null; 735 } 736 } 737 738 /** 739 * AsyncTask that moves temp audio file to the system user's {@link FileUtils#getPendingDir}. 740 * Once the task is completed, it either starts ACTION_COLLECT_BUGREPORT or updates the status 741 * to STATUS_UPLOAD_PENDING or STATUS_PENDING_USER_ACTION. 742 */ 743 private static class AddAudioToBugReportAsyncTask extends AsyncTask<Void, Void, Void> { 744 private final Context mContext; 745 private final Config mConfig; 746 private final File mTempAudioFile; 747 private boolean mIsNewBugReport; 748 private final boolean mIsFirstRecording; 749 private MetaBugReport mBugReport; 750 751 AddAudioToBugReportAsyncTask( 752 Context context, Config config, MetaBugReport bugReport, File tempAudioFile, 753 boolean isNewBugReport) { 754 mContext = context; 755 mConfig = config; 756 mBugReport = bugReport; 757 mTempAudioFile = tempAudioFile; 758 mIsNewBugReport = isNewBugReport; 759 mIsFirstRecording = Strings.isNullOrEmpty(mBugReport.getAudioFileName()); 760 } 761 762 @Override 763 protected Void doInBackground(Void... voids) { 764 String audioFileName = createFinalAudioFileName(); 765 mBugReport = BugStorageUtils.update(mContext, 766 mBugReport.toBuilder().setAudioFileName(audioFileName).build()); 767 try (OutputStream out = BugStorageUtils.openAudioMessageFileToWrite(mContext, 768 mBugReport); 769 InputStream input = new FileInputStream(mTempAudioFile)) { 770 ByteStreams.copy(input, out); 771 } catch (IOException e) { 772 // Allow user to try again if it fails to write audio. 773 BugStorageUtils.setBugReportStatus(mContext, mBugReport, 774 com.android.car.bugreport.Status.STATUS_AUDIO_PENDING, 775 "Failed to write audio to bug report"); 776 Log.e(TAG, "Failed to write audio to bug report", e); 777 return null; 778 } 779 780 mTempAudioFile.delete(); 781 return null; 782 } 783 784 /** 785 * Creates a final audio file name from temp audio file. 786 * 787 * For example, 788 * audio1128264677920904030.wav -> bugreport-Driver@2023-07-03_02-55-12-TLBZUR-message.wav 789 */ 790 private String createFinalAudioFileName() { 791 String audioFileExtension = mTempAudioFile.getName().substring( 792 mTempAudioFile.getName().lastIndexOf(".") + 1); 793 String audioTimestamp = MetaBugReport.toBugReportTimestamp(new Date()); 794 return FileUtils.getAudioFileName(audioTimestamp, mBugReport, audioFileExtension); 795 } 796 797 @Override 798 protected void onPostExecute(Void unused) { 799 super.onPostExecute(unused); 800 if (mIsNewBugReport) { 801 Log.i(TAG, "Starting bugreport service."); 802 startBugReportCollection(mBugReport.getId()); 803 } else { 804 if (mConfig.isAutoUpload()) { 805 BugStorageUtils.setBugReportStatus(mContext, mBugReport, 806 com.android.car.bugreport.Status.STATUS_UPLOAD_PENDING, ""); 807 } else { 808 BugStorageUtils.setBugReportStatus(mContext, mBugReport, 809 com.android.car.bugreport.Status.STATUS_PENDING_USER_ACTION, ""); 810 811 // If audio file name already exists, no need to show the finish notification 812 // again for audio replacement. 813 if (mIsFirstRecording) { 814 BugReportService.showBugReportFinishedNotification(mContext, mBugReport); 815 } 816 } 817 } 818 } 819 820 /** Starts the {@link BugReportService} to collect bug report. */ 821 private void startBugReportCollection(int bugReportId) { 822 Intent intent = new Intent(mContext, BugReportService.class); 823 intent.setAction(BugReportService.ACTION_COLLECT_BUGREPORT); 824 intent.putExtra(BugReportService.EXTRA_META_BUG_REPORT_ID, bugReportId); 825 mContext.startForegroundService(intent); 826 } 827 } 828 } 829