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