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