-
-
Notifications
You must be signed in to change notification settings - Fork 45
Add Audio recording foreground service #3057
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
778f121
Add audio recording service
avazirna 0975ead
Create notification when service starts
avazirna e384016
Refactor
avazirna 9447995
Initiate recording - Start service and bind to it
avazirna 1584fd0
Pause recording when a higher-priority recording starts
avazirna 632aab5
Handle pause/resume events
avazirna b3c7319
Stop and save recording
avazirna c8ed75b
Unbind service onDestroyView
avazirna e5c1c4a
Prevent service restart when killed
avazirna ffb128a
Unregister callback when destroying view
avazirna 4b8c7f4
Update service attributes
avazirna d3fe7e1
Lint
avazirna e19065c
Refactor
avazirna 3150811
Merge branch 'master' into add-audio-recording-service
avazirna 3693b17
Refactor
avazirna c74fa00
Set notification as non-dismissive
avazirna b5335ef
Lint
avazirna 6df91f4
Lint
avazirna File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
app/src/org/commcare/views/widgets/AudioRecordingHelper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package org.commcare.views.widgets; | ||
|
|
||
| import android.media.MediaCodecInfo; | ||
| import android.media.MediaCodecList; | ||
| import android.media.MediaRecorder; | ||
| import android.os.Build; | ||
|
|
||
| import org.commcare.util.LogTypes; | ||
| import org.javarosa.core.services.Logger; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| import static android.media.MediaFormat.MIMETYPE_AUDIO_AAC; | ||
|
|
||
| public class AudioRecordingHelper { | ||
| private static final int HEAAC_SAMPLE_RATE = 44100; | ||
| private static final int AMRNB_SAMPLE_RATE = 8000; | ||
|
|
||
| public MediaRecorder setupRecorder(String fileName) { | ||
| MediaRecorder recorder = new MediaRecorder(); | ||
|
|
||
| boolean isHeAacSupported = isHeAacEncoderSupported(); | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||
| recorder.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION); | ||
| } else { | ||
| recorder.setAudioSource(MediaRecorder.AudioSource.MIC); | ||
| } | ||
|
|
||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||
| recorder.setPrivacySensitive(true); | ||
| } | ||
| recorder.setAudioSamplingRate(isHeAacSupported ? HEAAC_SAMPLE_RATE : AMRNB_SAMPLE_RATE); | ||
| recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); | ||
| if (isHeAacSupported) { | ||
| recorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); | ||
| } else { | ||
| recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); | ||
| } | ||
| recorder.setOutputFile(fileName); | ||
|
|
||
| try { | ||
| recorder.prepare(); | ||
| Logger.log(LogTypes.TYPE_MEDIA_EVENT, "Preparing recording: " + fileName | ||
| + " | " + (isHeAacSupported ? HEAAC_SAMPLE_RATE : AMRNB_SAMPLE_RATE) | ||
| + " | " + (isHeAacSupported ? MediaRecorder.AudioEncoder.HE_AAC : | ||
| MediaRecorder.AudioEncoder.AMR_NB)); | ||
|
|
||
| } catch (IOException e) { | ||
| e.printStackTrace(); | ||
| } | ||
| return recorder; | ||
| } | ||
|
|
||
| // Checks whether the device supports High Efficiency AAC (HE-AAC) audio codec | ||
| private boolean isHeAacEncoderSupported() { | ||
| int numCodecs = MediaCodecList.getCodecCount(); | ||
|
|
||
| for (int i = 0; i < numCodecs; i++) { | ||
| MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); | ||
|
|
||
| if (!codecInfo.isEncoder()) { | ||
| continue; | ||
| } | ||
|
|
||
| for (String supportedType : codecInfo.getSupportedTypes()) { | ||
| if (supportedType.equalsIgnoreCase(MIMETYPE_AUDIO_AAC)) { | ||
| MediaCodecInfo.CodecCapabilities cap = codecInfo.getCapabilitiesForType(MIMETYPE_AUDIO_AAC); | ||
| MediaCodecInfo.CodecProfileLevel[] profileLevels = cap.profileLevels; | ||
| for (MediaCodecInfo.CodecProfileLevel profileLevel : profileLevels) { | ||
| int profile = profileLevel.profile; | ||
| if (profile == MediaCodecInfo.CodecProfileLevel.AACObjectHE | ||
| || profile == MediaCodecInfo.CodecProfileLevel.AACObjectHE_PS) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } |
139 changes: 139 additions & 0 deletions
139
app/src/org/commcare/views/widgets/AudioRecordingService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| package org.commcare.views.widgets; | ||
|
|
||
| import android.app.Notification; | ||
| import android.app.NotificationManager; | ||
| import android.app.PendingIntent; | ||
| import android.app.Service; | ||
| import android.content.Intent; | ||
| import android.content.pm.ServiceInfo; | ||
| import android.media.AudioRecordingConfiguration; | ||
| import android.media.MediaRecorder; | ||
| import android.os.Binder; | ||
| import android.os.Build; | ||
| import android.os.IBinder; | ||
|
|
||
| import androidx.annotation.Nullable; | ||
| import androidx.annotation.RequiresApi; | ||
| import androidx.core.app.NotificationCompat; | ||
|
|
||
| import org.commcare.CommCareNoficationManager; | ||
| import org.commcare.activities.DispatchActivity; | ||
| import org.commcare.dalvik.R; | ||
| import org.javarosa.core.services.locale.Localization; | ||
|
|
||
| /** | ||
| * A foreground service intended to be bound to the RecordingFragment for managing audio recording | ||
| * operations. Due to its persistent notification, the system treats it with higher importance, reducing the | ||
| * likelihood of interruptions during recordings. | ||
| * | ||
| * @author avazirna | ||
| **/ | ||
| public class AudioRecordingService extends Service { | ||
| private MediaRecorder recorder; | ||
| private final IBinder binder = new AudioRecorderBinder(); | ||
| public static final String RECORDING_FILENAME_EXTRA_KEY = "recording-filename-extra-key"; | ||
| private NotificationManager notificationManager; | ||
| private AudioRecordingHelper audioRecordingHelper = new AudioRecordingHelper(); | ||
|
|
||
| @Override | ||
| public void onCreate() { | ||
| super.onCreate(); | ||
| notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { | ||
| startForeground(RecordingFragment.RECORDING_NOTIFICATION_ID, createNotification(true), | ||
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE); | ||
| } else { | ||
| startForeground(RecordingFragment.RECORDING_NOTIFICATION_ID, createNotification(true)); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public int onStartCommand(Intent intent, int flags, int startId) { | ||
| String fileName = intent.getExtras().getString(RECORDING_FILENAME_EXTRA_KEY); | ||
| if (recorder == null) { | ||
| recorder = audioRecordingHelper.setupRecorder(fileName); | ||
| } | ||
| recorder.start(); | ||
| return START_NOT_STICKY; | ||
| } | ||
|
|
||
| @Override | ||
| public void onDestroy() { | ||
| resetRecorder(); | ||
| this.stopForeground(true); | ||
| } | ||
|
|
||
| private void resetRecorder() { | ||
| if (recorder != null) { | ||
| recorder.release(); | ||
| recorder = null; | ||
| } | ||
| } | ||
|
|
||
| private Notification createNotification(boolean recordingRunning) { | ||
| Intent activityToLaunch = new Intent(this, DispatchActivity.class); | ||
shubham1g5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| activityToLaunch.setAction("android.intent.action.MAIN"); | ||
| activityToLaunch.addCategory("android.intent.category.LAUNCHER"); | ||
|
|
||
| int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT; | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
| pendingIntentFlags = pendingIntentFlags | PendingIntent.FLAG_IMMUTABLE; | ||
| } | ||
| PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, activityToLaunch, pendingIntentFlags); | ||
|
|
||
| return new NotificationCompat.Builder(this, CommCareNoficationManager.NOTIFICATION_CHANNEL_USER_SESSION_ID) | ||
| .setContentTitle(Localization.get("recording.notification.title")) | ||
| .setContentText(recordingRunning ? Localization.get("recording.notification.in.progress") : | ||
| Localization.get("recording.notification.paused")) | ||
| .setSmallIcon(R.drawable.commcare_actionbar_logo) | ||
| .setContentIntent(pendingIntent) | ||
| .setOngoing(true) | ||
| .build(); | ||
| } | ||
|
|
||
| @Nullable | ||
| @Override | ||
| public IBinder onBind(Intent intent) { | ||
| return binder; | ||
| } | ||
|
|
||
| /** | ||
| * Provides other components with access to the functionality exposed by the AudioRecordingService | ||
| * | ||
| **/ | ||
| public class AudioRecorderBinder extends Binder { | ||
| public AudioRecordingService getService() { | ||
| return AudioRecordingService.this; | ||
| } | ||
| } | ||
|
|
||
| @RequiresApi(api = Build.VERSION_CODES.Q) | ||
| public AudioRecordingConfiguration getActiveRecordingConfiguration() { | ||
| if (!isRecorderActive()) { | ||
| return null; | ||
| } | ||
| return recorder.getActiveRecordingConfiguration(); | ||
| } | ||
|
|
||
| public boolean isRecorderActive() { | ||
| return recorder != null; | ||
| } | ||
|
|
||
| @RequiresApi(api = Build.VERSION_CODES.N) | ||
| public void pauseRecording() { | ||
| recorder.pause(); | ||
| notificationManager.notify(RecordingFragment.RECORDING_NOTIFICATION_ID, | ||
| createNotification(false)); | ||
| } | ||
|
|
||
| @RequiresApi(api = Build.VERSION_CODES.N) | ||
| public void resumeRecording() { | ||
| recorder.resume(); | ||
| notificationManager.notify(RecordingFragment.RECORDING_NOTIFICATION_ID, | ||
| createNotification(true)); | ||
| } | ||
|
|
||
| public void stopRecording() { | ||
| recorder.stop(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.