• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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