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