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