Skip to content

Commit

Permalink
Merge pull request #2752 from dimagi/handle-recording-configuration-c…
Browse files Browse the repository at this point in the history
…hanges

Improve audio recording configuration
  • Loading branch information
avazirna authored May 7, 2024
2 parents 2df7444 + 93e8e8f commit c440c43
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 23 deletions.
6 changes: 5 additions & 1 deletion app/assets/locales/android_translatable_strings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -444,20 +444,24 @@ custom.restore.file.not.set=Custom restore file path is not set in preferences
custom.restore.error=Error loading custom sync

start.recording=Start Recording
start.recording.failed=Unable to start recording while another application is recording!
stop.recording=Stop Recording
recording.header=Record a sound
before.recording=Tap to start recording
before.overwrite.recording=Tap to start a new recording
during.recording=Tap to stop recording
during.recording=Recording is in progress, avoid navigating away from CommCare. Tap to stop recording
after.recording=Recording complete.
delete.recording=Tap to delete recording
pause.recording=Recording paused, tap to continue recording
pause.recording.because.no.sound.captured=Recording paused because another app started recording, tap to continue recording
save=Save
recording.cancel=Cancel
recording.clear=Clear
recording.prompt.with.file.chooser=Record or choose sound below
recording.prompt.without.file.chooser=Record sound below
recording.custom=Recorded Sound
recording.paused.due.another.app.recording.title=CommCare Audio Recording
recording.paused.due.another.app.recording.message=Recording paused as another app started recording. Click here to resume the recording!

callout.failure.dialer=Device is not currently configured to make telephone calls
callout.failure.sms=SMS app not found
Expand Down
1 change: 1 addition & 0 deletions app/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -455,4 +455,5 @@
<string name="fcm_notification">FCM Notification</string>
<string name="fcm_default_notification_channel">notification-channel-push-notifications</string>
<string name="app_with_id_not_found">Required CommCare App is not installed on device</string>
<string name="audio_recording_notification">Audio Recording Notification</string>
</resources>
11 changes: 9 additions & 2 deletions app/src/org/commcare/utils/MediaUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;

import org.commcare.CommCareApplication;
import org.commcare.engine.references.JavaFileReference;
import org.commcare.google.services.analytics.AnalyticsParamValue;
import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
import org.commcare.preferences.HiddenPreferences;
import org.commcare.util.LogTypes;
import org.javarosa.core.reference.InvalidReferenceException;
Expand All @@ -27,6 +27,7 @@
import java.security.NoSuchAlgorithmException;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

/**
* @author ctsims
Expand Down Expand Up @@ -506,4 +507,10 @@ private static Pair<Bitmap, Boolean> performSafeScaleDown(String imageFilepath,
}
}

@RequiresApi(api = Build.VERSION_CODES.N)
public static boolean isRecordingActive(Context context){
return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
.getActiveRecordingConfigurations().size() > 0;
}

}
48 changes: 48 additions & 0 deletions app/src/org/commcare/utils/NotificationUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.commcare.utils;

import static android.content.Context.NOTIFICATION_SERVICE;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;

import androidx.core.app.NotificationCompat;

import org.commcare.activities.DispatchActivity;
import org.commcare.dalvik.R;

/**
* Set of methods to post notifications to the user.
*
* @author avazirna
*/
public class NotificationUtil {
public static void showNotification(Context context, String notificationChannel, int notificationId,
String notificationTitle, String notificationText, Intent actionIntent) {

int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
pendingIntentFlags = pendingIntentFlags | PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent contentIntent =
PendingIntent.getActivity(context, 0, actionIntent, pendingIntentFlags);

NotificationCompat.Builder notification =
new NotificationCompat.Builder(context, notificationChannel)
.setContentTitle(notificationTitle)
.setContentText(notificationText)
.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.notification)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setWhen(System.currentTimeMillis());
((NotificationManager) context.getSystemService(NOTIFICATION_SERVICE))
.notify(notificationId, notification.build());
}

public static void cancelNotification(Context context, int notificationId) {
((NotificationManager) context.getSystemService(NOTIFICATION_SERVICE))
.cancel(notificationId);
}
}
138 changes: 118 additions & 20 deletions app/src/org/commcare/views/widgets/RecordingFragment.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package org.commcare.views.widgets;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaPlayer;
Expand All @@ -23,17 +27,25 @@
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.DialogFragment;

import org.commcare.CommCareApplication;
import org.commcare.CommCareNoficationManager;
import org.commcare.activities.DispatchActivity;
import org.commcare.dalvik.R;
import org.commcare.utils.MediaUtil;
import org.commcare.utils.NotificationUtil;
import org.javarosa.core.services.locale.Localization;

import java.io.File;
import java.io.IOException;
import java.util.Date;

import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.DialogFragment;
import java.util.List;
import java.util.Optional;

/**
* A popup dialog fragment that handles recording_fragment and saving of audio
Expand All @@ -54,6 +66,7 @@ public class RecordingFragment extends DialogFragment {

private static final int HEAAC_SAMPLE_RATE = 44100;
private static final int AMRNB_SAMPLE_RATE = 8000;
private final int RECORDING_NOTIFICATION_ID = R.string.audio_recording_notification;

private String fileName;
private static final String FILE_EXT = ".mp3";
Expand All @@ -73,12 +86,12 @@ public class RecordingFragment extends DialogFragment {
private long mLastStopTime;
private boolean inPausedState = false;
private boolean savedRecordingExists = false;

private AudioManager.AudioRecordingCallback audioRecordingCallback;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
layout = (LinearLayout)inflater.inflate(R.layout.recording_fragment, container);
disableScreenRotation((AppCompatActivity)getContext());
layout = (LinearLayout) inflater.inflate(R.layout.recording_fragment, container);
disableScreenRotation((AppCompatActivity) getContext());
prepareButtons();
prepareText();
setWindowSize();
Expand Down Expand Up @@ -119,7 +132,7 @@ private void setWindowSize() {
Rect displayRectangle = new Rect();
Window window = getActivity().getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(displayRectangle);
layout.setMinimumWidth((int)(displayRectangle.width() * 0.9f));
layout.setMinimumWidth((int) (displayRectangle.width() * 0.9f));
getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
}

Expand All @@ -142,7 +155,6 @@ private void setActionText(String textKey) {
actionButton.setText(Localization.get(textKey));
}


private void resetRecordingView() {
if (recorder != null) {
recorder.release();
Expand All @@ -165,7 +177,14 @@ private void resetRecordingView() {
}

private void startRecording() {
disableScreenRotation((AppCompatActivity)getContext());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (MediaUtil.isRecordingActive(getContext())) {
Toast.makeText(getContext(), Localization.get("start.recording.failed"), Toast.LENGTH_SHORT).show();
return;
}
}

disableScreenRotation((AppCompatActivity) getContext());
setCancelable(false);
setupRecorder();
recorder.start();
Expand All @@ -177,7 +196,7 @@ private void recordingInProgress() {
recordingDuration.start();
if (isPauseSupported()) {
toggleRecording.setBackgroundResource(R.drawable.pause);
toggleRecording.setOnClickListener(v -> pauseRecording());
toggleRecording.setOnClickListener(v -> pauseRecording(true));
} else {
toggleRecording.setBackgroundResource(R.drawable.record_in_progress);
toggleRecording.setOnClickListener(v -> stopRecording());
Expand All @@ -189,15 +208,21 @@ private void recordingInProgress() {
discardRecording.setVisibility(View.INVISIBLE);
}


private void setupRecorder() {
if (recorder == null) {
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);
}

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);
recorder.setOutputFile(fileName);
Expand All @@ -206,6 +231,9 @@ private void setupRecorder() {
} else {
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
registerAudioRecordingConfigurationChangeCallback();
}
try {
recorder.prepare();
} catch (IOException e) {
Expand All @@ -230,7 +258,8 @@ private boolean isHeAacEncoderSupported() {
MediaCodecInfo.CodecProfileLevel[] profileLevels = cap.profileLevels;
for (MediaCodecInfo.CodecProfileLevel profileLevel : profileLevels) {
int profile = profileLevel.profile;
if (profile == MediaCodecInfo.CodecProfileLevel.AACObjectHE || profile == MediaCodecInfo.CodecProfileLevel.AACObjectHE_PS) {
if (profile == MediaCodecInfo.CodecProfileLevel.AACObjectHE
|| profile == MediaCodecInfo.CodecProfileLevel.AACObjectHE_PS) {
return true;
}
}
Expand Down Expand Up @@ -259,7 +288,7 @@ private void stopRecording() {
}

@SuppressLint("NewApi")
private void pauseRecording() {
private void pauseRecording(boolean pausedByUser) {
inPausedState = true;
recordingDuration.stop();
chronoPause();
Expand All @@ -268,7 +297,8 @@ private void pauseRecording() {
enableSave();
toggleRecording.setBackgroundResource(R.drawable.record_add);
toggleRecording.setOnClickListener(v -> resumeRecording());
instruction.setText(Localization.get("pause.recording"));
instruction.setText(Localization.get(pausedByUser ? "pause.recording"
: "pause.recording.because.no.sound.captured"));
}

private void enableSave() {
Expand All @@ -290,14 +320,16 @@ private boolean isPauseSupported() {
Bundle args = getArguments();
if (args != null) {
String appearance = args.getString(APPEARANCE_ATTR_ARG_KEY);
return LONG_APPEARANCE_VALUE.equals(appearance) &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
return LONG_APPEARANCE_VALUE.equals(appearance)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
return false;
}


private void saveRecording() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
unregisterAudioRecordingConfigurationChangeCallback();
}
if (inPausedState) {
stopRecording();
}
Expand All @@ -318,7 +350,7 @@ public void setListener(RecordingCompletionListener listener) {
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
enableScreenRotation((AppCompatActivity)getContext());
enableScreenRotation((AppCompatActivity) getContext());
if (recorder != null) {
recorder.release();
this.recorder = null;
Expand All @@ -328,7 +360,7 @@ public void onDismiss(DialogInterface dialog) {
try {
player.release();
} catch (IllegalStateException e) {
//Do nothing because player wasn't recording
// Do nothing because player wasn't recording
}
}
}
Expand Down Expand Up @@ -393,4 +425,70 @@ private void chronoResume() {
}
recordingDuration.start();
}

@RequiresApi(api = Build.VERSION_CODES.N)
private void registerAudioRecordingConfigurationChangeCallback() {
audioRecordingCallback = new AudioManager.AudioRecordingCallback() {
@Override
public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
super.onRecordingConfigChanged(configs);
if (recorder == null) {
return;
}

if (hasRecordingGoneSilent(configs)) {
if (!inPausedState) {
pauseRecording(false);
NotificationUtil.showNotification(
getContext(),
CommCareNoficationManager.NOTIFICATION_CHANNEL_USER_SESSION_ID,
RECORDING_NOTIFICATION_ID,
Localization.get("recording.paused.due.another.app.recording.title"),
Localization.get("recording.paused.due.another.app.recording.message"),
new Intent(getContext(), DispatchActivity.class)
.setAction(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER));
}
} else {
if (inPausedState) {
NotificationUtil.cancelNotification(getContext(), RECORDING_NOTIFICATION_ID);
}
}
}
};
((AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE))
.registerAudioRecordingCallback(audioRecordingCallback, null);
}

@RequiresApi(api = Build.VERSION_CODES.N)
private void unregisterAudioRecordingConfigurationChangeCallback() {
if (audioRecordingCallback != null) {
((AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE))
.unregisterAudioRecordingCallback(audioRecordingCallback);
audioRecordingCallback = null;
}
}

private boolean hasRecordingGoneSilent(List<AudioRecordingConfiguration> configs) {
if (recorder == null) {
return false;
}

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
if (recorder.getActiveRecordingConfiguration() == null) {
return false;
}

Optional<AudioRecordingConfiguration> currentAudioConfig = configs.stream().filter(config ->
config.getClientAudioSessionId() == recorder.getActiveRecordingConfiguration()
.getClientAudioSessionId())
.findAny();
return currentAudioConfig.isPresent() ? currentAudioConfig.get().isClientSilenced() : false;
} else {
if (recorder.getMaxAmplitude() == 0) {
return true;
}
return false;
}
}
}

0 comments on commit c440c43

Please sign in to comment.