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