Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />

<uses-feature
android:name="android.hardware.telephony"
Expand Down Expand Up @@ -342,6 +343,13 @@
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Service that maintains an encryption key in memory to securely submit data to the server" />
</service>
<service
android:enabled="true"
android:exported="false"
android:stopWithTask="true"
android:foregroundServiceType="microphone"
android:name="org.commcare.views.widgets.AudioRecordingService">
</service>
<activity
android:launchMode="singleTop"
android:name="org.commcare.activities.FormEntryActivity"
Expand Down
19 changes: 11 additions & 8 deletions app/assets/locales/android_translatable_strings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ verify.retry=Retry
verify.checking=Verifying media...
exception.during.verification=Verification could not complete due to the following exception. This error must be resolved before verification can proceed:\n\n${0}

barcode.reader.missing=No barcode reader available! You can install one from the android market.
barcode.reader.missing=No barcode reader available! You can install one from the Android market.

upgrade.button.retry=Retry Upgrade
upgrade.button.startover=Restart Upgrade
Expand Down Expand Up @@ -80,7 +80,7 @@ home.menu.validate=Validate Media
home.menu.locale.change=Change Language
home.menu.locale.select=Choose your Language
home.menu.formdump=Manage SD
home.menu.wifi.direct=Wifi Direct
home.menu.wifi.direct=Wi-Fi Direct
home.menu.connection.diagnostic=Connection Test
home.menu.saved.forms=Saved Forms
home.menu.about=About CommCare
Expand Down Expand Up @@ -119,7 +119,7 @@ sync.success.synced=Sync Successful! Your information is up to date.
sync.fail.unsent=Having issues communicating with the server to send forms. Will try again later.
sync.fail.timeout=CommCare didn't receive a response from the remote service in time, it may be busy! Please wait a while and try your request again later.
sync.fail.auth.loggedin=Password has changed. Please log in with updated credentials
sync.fail.empty.url=Sync url is not set. Please contact CommCare Support.
sync.fail.empty.url=Sync URL is not set. Please contact CommCare Support.
sync.fail.bad.data=Server provided improperly formatted data, please try again or contact your supervisor.
sync.fail.bad.local=Your information was fetched, but a problem occurred with the log in. Please try again.
sync.fail.bad.network=Couldn't contact server. Please make sure an internet connection is available or try again later.
Expand Down Expand Up @@ -406,7 +406,7 @@ connection.captive_portal.action=The current network is not connected to the int

notification.sync.connections.title=No Connection
notification.sync.connections.detail=No active data connections
notification.sync.connections.action=You don't have any active data connections configured. Try connecting your phone to wifi or turning mobile data on.
notification.sync.connections.action=You don't have any active data connections configured. Try connecting your phone to Wi-Fi or turning mobile data on.

multiple.apps.unverified.title=Missing Multimedia for all Apps
multiple.apps.unverified.message=All of your available apps are missing their associated multimedia resources. Please notify your supervisor so that he/she can fix this via the CommCare App Manager page.
Expand Down Expand Up @@ -465,6 +465,9 @@ 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!
recording.notification.title=Audio recording
recording.notification.in.progress=Recording in progress...
recording.notification.paused=Recording paused

callout.failure.dialer=Device is not currently configured to make telephone calls
callout.failure.sms=SMS app not found
Expand Down Expand Up @@ -514,8 +517,8 @@ refresh.build.settings.error=No refresh occurred. In order to use this utility,
login.attempt.fail.auth.title=Invalid Username or Password
login.attempt.fail.auth.detail=Your username or password was not recognized, please type them again and retry.

login.attempt.fail.empty.url.title=Url not set
login.attempt.fail.empty.url.detail=Sync url is empty. Please contact CommCare Support.
login.attempt.fail.empty.url.title=URL not set
login.attempt.fail.empty.url.detail=Sync URL is empty. Please contact CommCare Support.

login.attempt.fail.pin.title=Invalid PIN
login.attempt.fail.pin.detail=Please enter the 4 digit PIN number that you chose, or select "Forgot PIN?" in the options menu to login with your password instead.
Expand Down Expand Up @@ -785,9 +788,9 @@ wifi.direct.no.group=This device is not connected to any Wi-Fi Direct group.
wifi.direct.receive.successful=Received ${0} files successfully!
wifi.direct.send.successful=File Send Successful!
wifi.direct.send.unsuccessful=Error sending files: ${0}
wifi.direct.error.no.forms=Phone has received no forms via Wi-fi direct for Submitting; did you mean to Send forms?
wifi.direct.error.no.forms=Phone has received no forms via Wi-Fi direct for Submitting; did you mean to Send forms?
wifi.direct.discovery.start=Discovery Initiated
wifi.direct.discovery.failed.generic=Discovery failed due to bad Wi-fi state; turn Wi-fi on and off, then retry
wifi.direct.discovery.failed.generic=Discovery failed due to bad Wi-Fi state; turn Wi-Fi on and off, then retry
wifi.direct.discovery.failed.specific=Discovery failed with reason code ${0}
wifi.direct.connect.failed=Connecting failed, please retry
wifi.direct.error.wiping.forms=Error wiping forms: ${0}
Expand Down
82 changes: 82 additions & 0 deletions app/src/org/commcare/views/widgets/AudioRecordingHelper.java
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 app/src/org/commcare/views/widgets/AudioRecordingService.java
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);
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();
}
}
Loading
Loading