| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.google.android.car.bugreport; |
| |
| import static com.google.android.car.bugreport.BugReportService.EXTRA_META_BUG_REPORT; |
| import static com.google.android.car.bugreport.BugReportService.MAX_PROGRESS_VALUE; |
| |
| import android.Manifest; |
| import android.app.Activity; |
| import android.content.ComponentName; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.content.pm.PackageManager; |
| import android.media.MediaRecorder; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.UserManager; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.Window; |
| import android.widget.ProgressBar; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.text.DateFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.Random; |
| |
| /** |
| * Activity that shows two types of dialogs: starting a new bug report and current status of already |
| * in progress bug report. |
| * |
| * <p>If there is no in-progress bug report, it starts recording voice message. After clicking |
| * submit button it initiates {@link BugReportService}. |
| * |
| * <p>If bug report is in-progress, it shows a progress bar. |
| */ |
| public class BugReportActivity extends Activity { |
| private static final String TAG = BugReportActivity.class.getSimpleName(); |
| |
| private static final int VOICE_MESSAGE_MAX_DURATION_MILLIS = 60 * 1000; |
| private static final int AUDIO_PERMISSIONS_REQUEST_ID = 1; |
| |
| private static final DateFormat BUG_REPORT_TIMESTAMP_DATE_FORMAT = |
| new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); |
| |
| private final Handler mHandler = new Handler(Looper.getMainLooper()); |
| |
| private TextView mInProgressTitleText; |
| private ProgressBar mProgressBar; |
| private TextView mProgressText; |
| private VoiceRecordingView mVoiceRecordingView; |
| private View mVoiceRecordingFinishedView; |
| private View mSubmitBugReportLayout; |
| private View mInProgressLayout; |
| |
| private boolean mBound; |
| private boolean mAudioRecordingStarted; |
| private boolean mBugReportServiceStarted; |
| private BugReportService mService; |
| private MediaRecorder mRecorder; |
| private MetaBugReport mMetaBugReport; |
| |
| /** Defines callbacks for service binding, passed to bindService() */ |
| private ServiceConnection mConnection = new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName className, IBinder service) { |
| BugReportService.ServiceBinder binder = (BugReportService.ServiceBinder) service; |
| mService = binder.getService(); |
| mBound = true; |
| startAudioMessageRecording(); |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName arg0) { |
| // called when service connection breaks unexpectedly. |
| mBound = false; |
| } |
| }; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| requestWindowFeature(Window.FEATURE_NO_TITLE); |
| setContentView(R.layout.bug_report_activity); |
| |
| mInProgressTitleText = findViewById(R.id.in_progress_title_text); |
| mProgressBar = findViewById(R.id.progress_bar); |
| mProgressText = findViewById(R.id.progress_text); |
| mVoiceRecordingView = findViewById(R.id.voice_recording_view); |
| mVoiceRecordingFinishedView = findViewById(R.id.voice_recording_finished_text_view); |
| mSubmitBugReportLayout = findViewById(R.id.submit_bug_report_layout); |
| mInProgressLayout = findViewById(R.id.in_progress_layout); |
| |
| findViewById(R.id.button_submit).setOnClickListener(this::buttonSubmitClick); |
| findViewById(R.id.button_cancel).setOnClickListener(this::buttonCancelClick); |
| findViewById(R.id.button_close).setOnClickListener(this::buttonCancelClick); |
| |
| // Bind to BugReportService. |
| Intent intent = new Intent(this, BugReportService.class); |
| bindService(intent, mConnection, BIND_AUTO_CREATE); |
| } |
| |
| @Override |
| protected void onStart() { |
| super.onStart(); |
| |
| if (mBound) { |
| startAudioMessageRecording(); |
| } |
| } |
| |
| @Override |
| protected void onStop() { |
| super.onStop(); |
| if (!mBugReportServiceStarted && mAudioRecordingStarted) { |
| cancelAudioMessageRecording(); |
| } |
| if (mBound) { |
| mService.removeBugReportProgressListener(); |
| } |
| mAudioRecordingStarted = false; |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| |
| mHandler.removeCallbacksAndMessages(null); |
| if (mBound) { |
| unbindService(mConnection); |
| mBound = false; |
| } |
| } |
| |
| private void onProgressChanged(float progress) { |
| int progressValue = (int) progress; |
| mProgressBar.setProgress(progressValue); |
| mProgressText.setText(progressValue + "%"); |
| if (progressValue == MAX_PROGRESS_VALUE) { |
| mInProgressTitleText.setText(R.string.bugreport_dialog_in_progress_title_finished); |
| } |
| } |
| |
| private void showInProgressUi() { |
| mSubmitBugReportLayout.setVisibility(View.GONE); |
| mInProgressLayout.setVisibility(View.VISIBLE); |
| mInProgressTitleText.setText(R.string.bugreport_dialog_in_progress_title); |
| onProgressChanged(mService.getBugReportProgress()); |
| } |
| |
| private void showSubmitBugReportUi(boolean isRecording) { |
| mSubmitBugReportLayout.setVisibility(View.VISIBLE); |
| mInProgressLayout.setVisibility(View.GONE); |
| if (isRecording) { |
| mVoiceRecordingFinishedView.setVisibility(View.GONE); |
| mVoiceRecordingView.setVisibility(View.VISIBLE); |
| } else { |
| mVoiceRecordingFinishedView.setVisibility(View.VISIBLE); |
| mVoiceRecordingView.setVisibility(View.GONE); |
| } |
| } |
| |
| /** |
| * Initializes MetaBugReport in a local DB and starts audio recording. |
| * |
| * <p>This method expected to be called when the activity is started and bound to the service. |
| */ |
| private void startAudioMessageRecording() { |
| mService.setBugReportProgressListener(this::onProgressChanged); |
| |
| if (mService.isCollectingBugReport()) { |
| Log.i(TAG, "Bug report is already being collected."); |
| showInProgressUi(); |
| return; |
| } |
| |
| if (mAudioRecordingStarted) { |
| Log.i(TAG, "Audio message recording is already started."); |
| return; |
| } |
| |
| mAudioRecordingStarted = true; |
| showSubmitBugReportUi(/* isRecording= */ true); |
| |
| Date initiatedAt = new Date(); |
| String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt); |
| String username = getCurrentUserName(); |
| String title = BugReportTitleGenerator.generateBugReportTitle(initiatedAt, username); |
| mMetaBugReport = BugStorageUtils.createBugReport(this, title, timestamp, username); |
| |
| if (!hasRecordPermissions()) { |
| requestRecordPermissions(); |
| } else { |
| startRecordingWithPermission(); |
| } |
| } |
| |
| /** |
| * Cancels bugreporting by stopping audio recording and deleting temp files. |
| */ |
| private void cancelAudioMessageRecording() { |
| stopAudioRecording(); |
| File tempDir = FileUtils.getTempDir(this, mMetaBugReport.getTimestamp()); |
| Log.i(TAG, "Bug report is cancelled"); |
| new DeleteDirectoryAsyncTask().execute(tempDir); |
| BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_USER_CANCELLED, ""); |
| Toast.makeText(this, getString(R.string.toast_bugreport_cancelled), |
| Toast.LENGTH_SHORT).show(); |
| } |
| |
| private void buttonCancelClick(View view) { |
| finish(); |
| } |
| |
| private void buttonSubmitClick(View view) { |
| startBugReportingInService(); |
| finish(); |
| } |
| |
| private void startBugReportingInService() { |
| stopAudioRecording(); |
| Bundle bundle = new Bundle(); |
| bundle.putParcelable(EXTRA_META_BUG_REPORT, mMetaBugReport); |
| Intent intent = new Intent(this, BugReportService.class); |
| intent.putExtras(bundle); |
| startService(intent); |
| mBugReportServiceStarted = true; |
| } |
| |
| private void requestRecordPermissions() { |
| requestPermissions( |
| new String[]{Manifest.permission.RECORD_AUDIO}, AUDIO_PERMISSIONS_REQUEST_ID); |
| } |
| |
| private boolean hasRecordPermissions() { |
| return checkSelfPermission(Manifest.permission.RECORD_AUDIO) |
| == PackageManager.PERMISSION_GRANTED; |
| } |
| |
| @Override |
| public void onRequestPermissionsResult( |
| int requestCode, String[] permissions, int[] grantResults) { |
| if (requestCode != AUDIO_PERMISSIONS_REQUEST_ID) { |
| return; |
| } |
| for (int i = 0; i < grantResults.length; i++) { |
| if (Manifest.permission.RECORD_AUDIO.equals(permissions[i]) |
| && grantResults[i] == PackageManager.PERMISSION_GRANTED) { |
| startRecordingWithPermission(); |
| return; |
| } |
| } |
| handleNoPermission(permissions); |
| } |
| |
| private void handleNoPermission(String[] permissions) { |
| String text = this.getText(R.string.toast_permissions_denied) + " : " |
| + Arrays.toString(permissions); |
| Log.w(TAG, text); |
| Toast.makeText(this, text, Toast.LENGTH_LONG).show(); |
| finish(); |
| } |
| |
| private void startRecordingWithPermission() { |
| File recordingFile = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(), |
| "-message.3gp"); |
| Log.d(TAG, "start voice recording: " + recordingFile + ". activityObjectId" |
| + System.identityHashCode(this)); |
| |
| mRecorder = new MediaRecorder(); |
| mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); |
| mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); |
| mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); |
| mRecorder.setOutputFile(recordingFile); |
| |
| try { |
| mRecorder.prepare(); |
| } catch (IOException e) { |
| Log.e(TAG, "Failed on MediaRecorder#prepare(), filename: " + recordingFile, e); |
| finish(); |
| return; |
| } |
| |
| mRecorder.start(); |
| mVoiceRecordingView.setRecorder(mRecorder); |
| |
| mHandler.postDelayed(() -> { |
| Log.i(TAG, "Timed out while recording voice message, cancelling."); |
| stopAudioRecording(); |
| showSubmitBugReportUi(/* isRecording= */ false); |
| }, VOICE_MESSAGE_MAX_DURATION_MILLIS); |
| } |
| |
| private void stopAudioRecording() { |
| if (mRecorder != null) { |
| Log.i(TAG, "Recording ended, stopping the MediaRecorder."); |
| mRecorder.stop(); |
| mRecorder.release(); |
| mRecorder = null; |
| } |
| mVoiceRecordingView.setRecorder(null); |
| } |
| |
| private String getCurrentUserName() { |
| UserManager um = UserManager.get(this); |
| return um.getUserName(); |
| } |
| |
| /** A helper class to generate bugreport title. */ |
| private static final class BugReportTitleGenerator { |
| /** Contains easily readable characters. */ |
| private static final char[] CHARS_FOR_RANDOM_GENERATOR = |
| new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', |
| 'R', 'S', 'T', 'U', 'W', 'X', 'Y', 'Z'}; |
| |
| private static final int LOOKUP_STRING_LENGTH = 6; |
| |
| /** |
| * Generates a bugreport title from given timestamp and username. |
| * |
| * <p>Example: "[A45E8] Feedback from user Driver at 2019-09-21_12:00:00" |
| */ |
| static String generateBugReportTitle(Date initiatedAt, String username) { |
| // Lookup string is used to search a bug in Buganizer (see b/130915969). |
| String lookupString = generateRandomString(LOOKUP_STRING_LENGTH); |
| String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt); |
| return "[" + lookupString + "] Feedback from user " + username + " at " + timestamp; |
| } |
| |
| private static String generateRandomString(int length) { |
| Random random = new Random(); |
| StringBuilder builder = new StringBuilder(); |
| for (int i = 0; i < length; i++) { |
| int randomIndex = random.nextInt(CHARS_FOR_RANDOM_GENERATOR.length); |
| builder.append(CHARS_FOR_RANDOM_GENERATOR[randomIndex]); |
| } |
| return builder.toString(); |
| } |
| } |
| |
| /** AsyncTask that recursively deletes directories. */ |
| private static class DeleteDirectoryAsyncTask extends AsyncTask<File, Void, Void> { |
| @Override |
| protected Void doInBackground(File... files) { |
| for (File file : files) { |
| Log.d(TAG, "Deleting " + file.getAbsolutePath()); |
| FileUtils.deleteDirectory(file); |
| } |
| return null; |
| } |
| } |
| } |