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