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