• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 
17 package com.google.android.car.kitchensink.audiorecorder;
18 
19 import static android.R.layout.simple_spinner_dropdown_item;
20 import static android.R.layout.simple_spinner_item;
21 
22 import android.Manifest;
23 import android.content.ClipData;
24 import android.content.ClipboardManager;
25 import android.content.pm.PackageManager;
26 import android.media.AudioDeviceInfo;
27 import android.media.AudioManager;
28 import android.media.MediaPlayer;
29 import android.media.MediaRecorder;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.util.IndentingPrintWriter;
33 import android.util.Log;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.AdapterView;
38 import android.widget.ArrayAdapter;
39 import android.widget.Button;
40 import android.widget.Spinner;
41 import android.widget.TextView;
42 
43 import androidx.activity.result.ActivityResultLauncher;
44 import androidx.activity.result.contract.ActivityResultContracts;
45 import androidx.fragment.app.Fragment;
46 
47 import com.google.android.car.kitchensink.KitchenSinkActivity;
48 import com.google.android.car.kitchensink.R;
49 
50 import java.io.FileDescriptor;
51 import java.io.IOException;
52 import java.io.PrintWriter;
53 import java.time.Instant;
54 import java.time.ZoneId;
55 import java.time.format.DateTimeFormatter;
56 import java.util.Arrays;
57 import java.util.Map;
58 
59 public final class AudioRecorderTestFragment extends Fragment {
60     public static final String DUMP_ARG_CMD = "cmd";
61     public static final String FRAGMENT_NAME = "audio recorder";
62     private static final String TAG = "CAR.AUDIO.RECORDER.KS";
63     private static final String[] PERMISSIONS = {Manifest.permission.RECORD_AUDIO};
64     private static final String PATTERN_FORMAT = "yyyy_MM_dd_kk_mm_ss_";
65 
66     private final Map<String, DumpCommand> mDumpCommands = Map.ofEntries(
67             Map.entry("start-recording",
68                     new DumpCommand("start-recording", "Starts recording audio to file.") {
69                         @Override
70                         boolean runCommand(IndentingPrintWriter writer) {
71                             startRecording();
72                             writer.println("Started recording");
73                             return true;
74                         }
75                     }),
76             Map.entry("stop-recording",
77                     new DumpCommand("stop-recording", "Stops recording audio to file.") {
78                         @Override
79                         boolean runCommand(IndentingPrintWriter writer) {
80                             stopRecording();
81                             writer.println("Stopped recording");
82                             return true;
83                         }
84                     }),
85             Map.entry("start-playback",
86                     new DumpCommand("start-playback", "Start audio playback.") {
87                         @Override
88                         boolean runCommand(IndentingPrintWriter writer) {
89                             startPlayback();
90                             writer.println("Started playback");
91                             return true;
92                         }
93                     }),
94             Map.entry("stop-playback",
95                     new DumpCommand("stop-playback", "Stop audio playback.") {
96                         @Override
97                         boolean runCommand(IndentingPrintWriter writer) {
98                             stopPlayback();
99                             writer.println("Stopped Playback");
100                             return true;
101                         }
102                     }),
103             Map.entry("help",
104                     new DumpCommand("help", "Print help information.") {
105                         @Override
106                         boolean runCommand(IndentingPrintWriter writer) {
107                             dumpHelp(writer);
108                             return true;
109                         }
110                     }));
111 
112     private Spinner mDeviceAddressSpinner;
113     private ArrayAdapter<AudioDeviceInfoWrapper> mDeviceAddressAdapter;
114     private MediaRecorder mMediaRecorder;
115     private TextView mStatusTextView;
116     private TextView mFilePathTextView;
117     private String mFileName = "";
118     private MediaPlayer mMediaPlayer;
119 
120     private final ActivityResultLauncher<String[]> mRequestPermissionLauncher =
121             registerForActivityResult(
122                     new ActivityResultContracts.RequestMultiplePermissions(), permissions -> {
123                         boolean allGranted = false;
124                         for (String permission : permissions.keySet()) {
125                             boolean granted = permissions.get(permission);
126                             Log.d(TAG, "permission [" + permission + "] granted " + granted);
127                             allGranted = allGranted && granted;
128                         }
129 
130                         if (allGranted) {
131                             setStatus("All Permissions Granted");
132                             return;
133                         }
134                         setStatus("Not All Permissions Granted");
135                     });
136 
137     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle)138     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
139         Log.d(TAG, "onCreateView");
140         View view = inflater.inflate(R.layout.audio_recorder, container, /* attachToRoo= */ false);
141 
142         initTextViews(view);
143         initButtons(view);
144         initInputDevices(view);
145         hasPermissionRequestIfNeeded();
146 
147         return view;
148     }
149 
150     @Override
onDestroyView()151     public void onDestroyView() {
152         super.onDestroyView();
153         Log.d(TAG, "onDestroyView");
154 
155         stopRecording();
156         stopPlayback();
157     }
158 
159     @Override
dump(String prefix, FileDescriptor fd, PrintWriter printWriter, String[] args)160     public void dump(String prefix, FileDescriptor fd, PrintWriter printWriter, String[] args) {
161         IndentingPrintWriter writer = new IndentingPrintWriter(printWriter, /* prefix= */ "  ");
162         if (args != null && args.length > 0) {
163             runDumpCommand(writer, args);
164             return;
165         }
166         writer.println(AudioRecorderTestFragment.class.getSimpleName());
167         writer.increaseIndent();
168         dumpRecordingState(writer);
169         dumpPlaybackState(writer);
170         writer.decreaseIndent();
171     }
172 
runDumpCommand(IndentingPrintWriter writer, String[] args)173     private void runDumpCommand(IndentingPrintWriter writer, String[] args) {
174         if (args.length > 1 && args[0].equals(DUMP_ARG_CMD) && mDumpCommands.containsKey(args[1])) {
175             String commandString = args[1];
176             DumpCommand command = mDumpCommands.get(commandString);
177             if (command.supportsCommand(commandString) && command.runCommand(writer)) {
178                 return;
179             }
180         }
181         dumpHelp(writer);
182     }
183 
dumpHelp(IndentingPrintWriter writer)184     private void dumpHelp(IndentingPrintWriter writer) {
185         writer.printf("adb shell 'dumpsys activity %s/.%s fragment \"%s\" cmd <command>'\n\n",
186                 KitchenSinkActivity.class.getPackage().getName(),
187                 KitchenSinkActivity.class.getSimpleName(),
188                 FRAGMENT_NAME);
189         writer.increaseIndent();
190         writer.printf("Supported commands: \n");
191         writer.increaseIndent();
192         for (DumpCommand command : mDumpCommands.values()) {
193             writer.printf("%s\n", command);
194         }
195         writer.decreaseIndent();
196         writer.decreaseIndent();
197     }
198 
dumpPlaybackState(PrintWriter writer)199     private void dumpPlaybackState(PrintWriter writer) {
200         writer.printf("Is playing: %s\n", (mMediaPlayer != null && mMediaPlayer.isPlaying()));
201     }
202 
dumpRecordingState(PrintWriter writer)203     private void dumpRecordingState(PrintWriter writer) {
204         writer.printf("Is recording: %s\n", mMediaRecorder != null);
205         writer.printf("Recording path: %s\n", getFilePath());
206         writer.printf("Adb command: %s\n", getFileCopyAdbCommand());
207     }
208 
initTextViews(View view)209     private void initTextViews(View view) {
210         mStatusTextView = view.findViewById(R.id.status_text_view);
211         mFilePathTextView = view.findViewById(R.id.file_path_edit);
212         mFilePathTextView.setOnClickListener(v -> {
213             ClipboardManager clipboard = getContext().getSystemService(ClipboardManager.class);
214             ClipData clip = ClipData.newPlainText("adb copy command", getFileCopyAdbCommand());
215             clipboard.setPrimaryClip(clip);
216         });
217     }
218 
getFileCopyAdbCommand()219     private String getFileCopyAdbCommand() {
220         return "adb pull -s " + Build.getSerial() + " " + getFilePath();
221     }
222 
getFilePath()223     private String getFilePath() {
224         return mFilePathTextView.getText().toString();
225     }
226 
setStatus(String status)227     private void setStatus(String status) {
228         mStatusTextView.setText(status);
229         Log.d(TAG, "setStatus " + status);
230     }
231 
setFilePath(String path)232     private void setFilePath(String path) {
233         Log.d(TAG, "setFilePath: " + path);
234         mFilePathTextView.setText(path);
235     }
236 
initButtons(View view)237     private void initButtons(View view) {
238         Log.d(TAG, "initButtons");
239 
240         setListenerForButton(view, R.id.button_start_input, v -> startRecording());
241         setListenerForButton(view, R.id.button_stop_input, v -> stopRecording());
242         setListenerForButton(view , R.id.button_start_playback, v -> startPlayback());
243         setListenerForButton(view, R.id.button_stop_playback, v -> stopPlayback());
244     }
245 
setListenerForButton(View view, int resourceId, View.OnClickListener listener)246     private void setListenerForButton(View view, int resourceId, View.OnClickListener listener) {
247         Button stopPlaybackButton = view.findViewById(resourceId);
248         stopPlaybackButton.setOnClickListener(listener);
249     }
250 
startPlayback()251     private void startPlayback() {
252         Log.d(TAG, "startPlayback " + mFileName);
253 
254         if (mMediaRecorder != null) {
255             setStatus("Still recording, stop first");
256             return;
257         }
258 
259         if (mFileName.isEmpty()) {
260             setStatus("No recording available");
261             return;
262         }
263 
264         MediaPlayer mediaPlayer = new MediaPlayer();
265 
266         try {
267             mediaPlayer.setDataSource(mFileName);
268             mediaPlayer.setOnCompletionListener(mediaPlayer1 -> stopPlayback());
269             mediaPlayer.prepare();
270             mediaPlayer.start();
271         } catch (IOException e) {
272             Log.e(TAG, "startPlayback media player failed", e);
273         }
274 
275         mMediaPlayer = mediaPlayer;
276         setStatus("Started playback");
277     }
278 
stopPlayback()279     private void stopPlayback() {
280         Log.d(TAG, "stopPlayback");
281 
282         if (mMediaPlayer == null) {
283             setStatus("Playback stopped");
284             return;
285         }
286 
287         mMediaPlayer.stop();
288         mMediaPlayer = null;
289         setStatus("Stopped playback");
290     }
291 
hasPermissionRequestIfNeeded()292     private boolean hasPermissionRequestIfNeeded() {
293         Log.d(TAG, "hasPermissionRequestIfNeeded");
294 
295         boolean allPermissionsGranted = true;
296 
297         for (String requiredPermission : PERMISSIONS) {
298             int checkValue = getContext().checkCallingOrSelfPermission(requiredPermission);
299             Log.d(TAG, "hasPermissionRequestIfNeeded " + requiredPermission + " granted "
300                     + (checkValue == PackageManager.PERMISSION_GRANTED));
301 
302             allPermissionsGranted = allPermissionsGranted
303                     && (checkValue == PackageManager.PERMISSION_GRANTED);
304         }
305 
306         if (allPermissionsGranted) {
307             return true;
308         }
309 
310         mRequestPermissionLauncher.launch(PERMISSIONS);
311         return false;
312     }
313 
initInputDevices(View view)314     private void initInputDevices(View view) {
315         Log.d(TAG, "initInputDevices");
316 
317         AudioManager audioManager = getContext().getSystemService(AudioManager.class);
318 
319         AudioDeviceInfo[] audioDeviceInfos =
320                 audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
321         AudioDeviceInfoWrapper[] audioDeviceInfoWrappers =
322                 Arrays.stream(audioDeviceInfos).map(AudioDeviceInfoWrapper::new)
323                 .toArray(AudioDeviceInfoWrapper[]::new);
324 
325         mDeviceAddressSpinner = view.findViewById(R.id.device_spinner);
326 
327         mDeviceAddressAdapter =
328                 new ArrayAdapter<>(getContext(), simple_spinner_item, audioDeviceInfoWrappers);
329         mDeviceAddressAdapter.setDropDownViewResource(
330                 simple_spinner_dropdown_item);
331 
332         mDeviceAddressSpinner.setAdapter(mDeviceAddressAdapter);
333 
334         mDeviceAddressSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
335             @Override
336             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
337                 stopRecording();
338             }
339 
340             @Override
341             public void onNothingSelected(AdapterView<?> parent) {
342                 Log.d(TAG, "onNothingSelected");
343             }
344         });
345     }
346 
stopRecording()347     private void stopRecording() {
348         Log.d(TAG, "stopRecording");
349 
350         if (mMediaRecorder == null) {
351             setStatus("stopRecording already stopped");
352             return;
353         }
354 
355         mMediaRecorder.stop();
356         mMediaRecorder = null;
357         setStatus("stopRecording recorder stopped");
358     }
359 
startRecording()360     private void startRecording() {
361         Log.d(TAG, "startRecording");
362 
363         if (!hasPermissionRequestIfNeeded()) {
364             Log.w(TAG, "startRecording missing permission");
365             return;
366         }
367 
368         AudioDeviceInfoWrapper audioInputDeviceInfoWrapper = mDeviceAddressAdapter.getItem(
369                 mDeviceAddressSpinner.getSelectedItemPosition());
370 
371         String fileName = getFileName(audioInputDeviceInfoWrapper);
372 
373         Log.d(TAG, "startRecording file name " + fileName);
374 
375         MediaRecorder recorder = new MediaRecorder(getContext());
376         recorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT);
377         recorder.setPreferredDevice(audioInputDeviceInfoWrapper.getAudioDeviceInfo());
378         recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
379         recorder.setOutputFile(fileName);
380         recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
381 
382         try {
383             recorder.prepare();
384         } catch (IOException e) {
385             Log.e(TAG, "startRecording prepare failed", e);
386             return;
387         }
388 
389         recorder.start();
390 
391         mFileName = fileName;
392         mMediaRecorder = recorder;
393         setFilePath(mFileName);
394         setStatus("Recording Started");
395     }
396 
getFileName( AudioDeviceInfoWrapper audioInputDeviceInfoWrapper)397     private String getFileName(
398             AudioDeviceInfoWrapper audioInputDeviceInfoWrapper) {
399         DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PATTERN_FORMAT)
400                 .withZone(ZoneId.systemDefault());
401         String shortName = formatter.format(Instant.now())
402                 + audioInputDeviceInfoWrapper.toStringNoSymbols();
403         return getActivity().getCacheDir().getAbsolutePath() + "/" + shortName + ".mp4";
404     }
405 
406     private static final class AudioDeviceInfoWrapper {
407 
408         private final AudioDeviceInfo mAudioDeviceInfo;
409 
AudioDeviceInfoWrapper(AudioDeviceInfo audioDeviceInfo)410         AudioDeviceInfoWrapper(AudioDeviceInfo audioDeviceInfo) {
411             mAudioDeviceInfo = audioDeviceInfo;
412         }
413 
getAudioDeviceInfo()414         AudioDeviceInfo getAudioDeviceInfo() {
415             return mAudioDeviceInfo;
416         }
417 
418         @Override
toString()419         public String toString() {
420             StringBuilder builder = new StringBuilder()
421                     .append("Type: ")
422                     .append(typeToString(mAudioDeviceInfo.getType()));
423 
424             if (!mAudioDeviceInfo.getAddress().isEmpty()) {
425                 builder.append(", Address: ");
426                 builder.append(mAudioDeviceInfo.getAddress());
427             }
428 
429             return builder.toString();
430         }
431 
toStringNoSymbols()432         public String toStringNoSymbols() {
433             StringBuilder builder = new StringBuilder();
434 
435             if (!mAudioDeviceInfo.getAddress().isEmpty()) {
436                 builder.append("address_");
437                 builder.append(mAudioDeviceInfo.getAddress().replace("//s", "_"));
438             } else {
439                 builder.append("type_");
440                 builder.append(typeToString(mAudioDeviceInfo.getType()));
441             }
442 
443             return builder.toString();
444         }
445 
typeToString(int type)446         static String typeToString(int type) {
447             switch (type) {
448                 case AudioDeviceInfo.TYPE_BUILTIN_MIC:
449                     return "MIC";
450                 case AudioDeviceInfo.TYPE_FM_TUNER:
451                     return "FM_TUNER";
452                 case AudioDeviceInfo.TYPE_AUX_LINE:
453                     return "AUX_LINE";
454                 case AudioDeviceInfo.TYPE_ECHO_REFERENCE:
455                     return "ECHO_REFERENCE";
456                 case AudioDeviceInfo.TYPE_BUS:
457                     return "BUS";
458                 case AudioDeviceInfo.TYPE_REMOTE_SUBMIX:
459                     return "REMOTE_SUBMIX";
460                 default:
461                     return "TYPE[" + type + "]";
462             }
463         }
464     }
465 
466     private abstract class DumpCommand {
467 
468         private final String mDescription;
469         private final String mCommand;
470 
DumpCommand(String command, String description)471         DumpCommand(String command, String description) {
472             mCommand = command;
473             mDescription = description;
474         }
475 
supportsCommand(String command)476         boolean supportsCommand(String command) {
477             return mCommand.equals(command);
478         }
479 
runCommand(IndentingPrintWriter writer)480         abstract boolean runCommand(IndentingPrintWriter writer);
481 
482         @Override
toString()483         public String toString() {
484             return new StringBuilder()
485                     .append(mCommand)
486                     .append(": ")
487                     .append(mDescription)
488                     .toString();
489         }
490     }
491 }
492