-
-
Save Venryx/e1f772b4c05b2da08e118ccd5cc162ff to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="utf-8"?> | |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.myapp"> | |
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher android:label="@string/app_name" | |
android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> | |
<service android:name=".ForegroundService" android:enabled="true" android:exported="true"></service> | |
<activity | |
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale" | |
android:name="com.myapp.MainActivity" | |
android:label="@string/title_activity_main" | |
android:theme="@style/AppTheme.NoActionBarLaunch" | |
android:launchMode="singleTop"> | |
<!-- //android:launchMode="singleTask" --> | |
<intent-filter> | |
<action android:name="android.intent.action.MAIN" /> | |
<category android:name="android.intent.category.LAUNCHER" /> | |
</intent-filter> | |
<intent-filter> | |
<action android:name="android.intent.action.VIEW" /> | |
<category android:name="android.intent.category.DEFAULT" /> | |
<category android:name="android.intent.category.BROWSABLE" /> | |
<data android:scheme="@string/custom_url_scheme" /> | |
</intent-filter> | |
</activity> | |
<provider | |
android:name="android.support.v4.content.FileProvider" | |
android:authorities="${applicationId}.fileprovider" | |
android:exported="false" | |
android:grantUriPermissions="true"> | |
<meta-data | |
android:name="android.support.FILE_PROVIDER_PATHS" | |
android:resource="@xml/file_paths"></meta-data> | |
</provider> | |
</application> | |
<!-- Permissions --> | |
<uses-permission android:name="android.permission.INTERNET" /> | |
<!-- Camera, Photos, input file --> | |
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> | |
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | |
<!-- Geolocation API --> | |
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> | |
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> | |
<uses-feature android:name="android.hardware.location.gps" /> | |
<!-- Network API --> | |
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | |
<!-- Navigator.getUserMedia --> | |
<!-- Video --> | |
<uses-permission android:name="android.permission.CAMERA" /> | |
<!-- Audio --> | |
<uses-permission android:name="android.permission.RECORD_AUDIO" /> | |
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> | |
<!-- v-added --> | |
<uses-permission android:name="android.permission.WAKE_LOCK" /> | |
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | |
</manifest> |
package com.myapp; | |
import android.app.Notification; | |
import android.app.NotificationChannel; | |
import android.app.NotificationManager; | |
import android.app.PendingIntent; | |
import android.app.Service; | |
import android.content.Intent; | |
import android.media.AudioFormat; | |
import android.media.AudioManager; | |
import android.media.AudioRecord; | |
import android.media.AudioTrack; | |
import android.media.MediaRecorder; | |
import android.os.Build; | |
import android.os.IBinder; | |
import android.support.annotation.Nullable; | |
import android.support.v4.app.NotificationCompat; | |
import android.util.Log; | |
import com.getcapacitor.PluginCall; | |
import com.getcapacitor.PluginMethod; | |
public class ForegroundService extends Service { | |
public static final String CHANNEL_ID = "ForegroundServiceChannel"; | |
@Override | |
public void onCreate() { | |
super.onCreate(); | |
} | |
@Override | |
public int onStartCommand(Intent intent, int flags, int startId) { | |
String input = intent.getStringExtra("inputExtra"); | |
createNotificationChannel(); | |
Intent notificationIntent = new Intent(this, MainActivity.class); | |
PendingIntent pendingIntent = PendingIntent.getActivity(this, | |
0, notificationIntent, 0); | |
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) | |
.setContentTitle("Foreground Service") | |
.setContentText(input) | |
.setSmallIcon(R.drawable.ic_launcher_foreground) | |
.setContentIntent(pendingIntent) | |
.build(); | |
startForeground(1, notification); | |
// do heavy work on a background thread | |
StartRecorder(); | |
//stopSelf(); | |
return START_NOT_STICKY; | |
} | |
@Override | |
public void onDestroy() { | |
super.onDestroy(); | |
} | |
@Nullable | |
@Override | |
public IBinder onBind(Intent intent) { | |
return null; | |
} | |
private void createNotificationChannel() { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
NotificationChannel serviceChannel = new NotificationChannel( | |
CHANNEL_ID, | |
"Foreground Service Channel", | |
NotificationManager.IMPORTANCE_DEFAULT | |
); | |
NotificationManager manager = getSystemService(NotificationManager.class); | |
manager.createNotificationChannel(serviceChannel); | |
} | |
} | |
private static String TAG = "ForegroundService"; | |
// the audio recording options | |
private static final int RECORDING_RATE = 44100; | |
private static final int CHANNEL = AudioFormat.CHANNEL_IN_MONO; | |
private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; | |
// the audio recorder | |
private AudioRecord recorder; | |
// the minimum buffer size needed for audio recording | |
private static int BUFFER_SIZE = AudioRecord.getMinBufferSize(RECORDING_RATE, CHANNEL, FORMAT); | |
// are we currently sending audio data | |
private boolean currentlySendingAudio = false; | |
public void StartRecorder() { | |
Log.i(TAG, "Starting the audio stream"); | |
currentlySendingAudio = true; | |
startStreaming(); | |
} | |
public void StopRecorder() { | |
Log.i(TAG, "Stopping the audio stream"); | |
currentlySendingAudio = false; | |
recorder.release(); | |
} | |
private void startStreaming() { | |
Log.i(TAG, "Starting the background thread (in this foreground service) to read the audio data"); | |
Thread streamThread = new Thread(() -> { | |
try { | |
Log.d(TAG, "Creating the buffer of size " + BUFFER_SIZE); | |
//byte[] buffer = new byte[BUFFER_SIZE]; | |
int rate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_SYSTEM); | |
int bufferSize = AudioRecord.getMinBufferSize(rate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); | |
short[] buffer = new short[bufferSize]; | |
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); | |
Log.d(TAG, "Creating the AudioRecord"); | |
//recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, RECORDING_RATE, CHANNEL, FORMAT, BUFFER_SIZE * 10); | |
recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, rate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); | |
Log.d(TAG, "AudioRecord recording..."); | |
recorder.startRecording(); | |
while (currentlySendingAudio == true) { | |
// read the data into the buffer | |
int readSize = recorder.read(buffer, 0, buffer.length); | |
double maxAmplitude = 0; | |
for (int i = 0; i < readSize; i++) { | |
if (Math.abs(buffer[i]) > maxAmplitude) { | |
maxAmplitude = Math.abs(buffer[i]); | |
} | |
} | |
double db = 0; | |
if (maxAmplitude != 0) { | |
db = 20.0 * Math.log10(maxAmplitude / 32767.0) + 90; | |
} | |
Log.d(TAG, "Max amplitude: " + maxAmplitude + " ; DB: " + db); | |
} | |
Log.d(TAG, "AudioRecord finished recording"); | |
} catch (Exception e) { | |
Log.e(TAG, "Exception: " + e); | |
} | |
}); | |
// start the thread | |
streamThread.start(); | |
} | |
} |
package com.myapp; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.media.AudioFormat; | |
import android.media.AudioManager; | |
import android.media.AudioRecord; | |
import android.media.AudioTrack; | |
import android.media.MediaRecorder; | |
import android.net.wifi.WifiManager; | |
import android.nfc.Tag; | |
import android.os.PowerManager; | |
import android.support.v4.content.ContextCompat; | |
import android.util.Log; | |
import android.widget.Button; | |
import com.getcapacitor.JSObject; | |
import com.getcapacitor.NativePlugin; | |
import com.getcapacitor.Plugin; | |
import com.getcapacitor.PluginCall; | |
import com.getcapacitor.PluginMethod; | |
import com.getcapacitor.PluginResult; | |
import java.net.DatagramSocket; | |
@NativePlugin() | |
public class General extends Plugin { | |
private static String TAG = "V.General"; | |
@PluginMethod | |
public void StartRecorder(PluginCall call) { | |
Log.i(TAG, "Starting the foreground-thread"); | |
Intent serviceIntent = new Intent(getActivity().getApplicationContext(), ForegroundService.class); | |
serviceIntent.putExtra("inputExtra", "Foreground Service Example in Android"); | |
ContextCompat.startForegroundService(getActivity(), serviceIntent); | |
call.resolve(); | |
} | |
@PluginMethod | |
public void StopRecorder(PluginCall call) { | |
Log.i(TAG, "Stopping the foreground-thread"); | |
Intent serviceIntent = new Intent(getActivity().getApplicationContext(), ForegroundService.class); | |
getActivity().getApplicationContext().stopService(serviceIntent); | |
call.resolve(); | |
} | |
// From what I've seen you don't need the wake-lock or wifi-lock below for the audio-recorder to persist through screen-off. | |
// However, to be on the safe side you might want to activate them anyway. (and/or if you have other functions that need them) | |
private PowerManager.WakeLock wakeLock_partial = null; | |
public void StartPartialWakeLock() { | |
if (wakeLock_partial != null && wakeLock_partial.isHeld()) return; | |
Log.i("vmain", "Starting partial wake-lock."); | |
final PowerManager pm = (PowerManager) getActivity().getApplicationContext().getSystemService(Context.POWER_SERVICE); | |
wakeLock_partial = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "com.myapp:partial_wake_lock"); | |
wakeLock_partial.acquire(); | |
} | |
public void StopPartialWakeLock() { | |
if (wakeLock_partial != null && wakeLock_partial.isHeld()) { | |
Log.i("vmain", "Stopping partial wake-lock."); | |
wakeLock_partial.release(); | |
} | |
} | |
private WifiManager.WifiLock wifiLock = null; | |
public void StartWifiLock() { | |
WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(Context.WIFI_SERVICE); | |
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "LockTag"); | |
wifiLock.acquire(); | |
} | |
public void StopWifiLock() { | |
wifiLock.release(); | |
} | |
} |
Has anyone tried this working code?
Okay so after some years, I have returned to some private Android projects, and had need of the "record with screen off" functionality again (on newer Android versions).
And I hit the issue mentioned above, of recording in background not working with the gist code as-is. I checked out the page linked by @venkateshpullaganti , and indeed, that got record-with-screen-off working again. (for my phone, which is on Android 14)
Specifically, look at the changes the react-native-callkeep project made here: https://github.com/react-native-webrtc/react-native-callkeep/pull/321/files
If you already have a foreground-service, you have to:
- Add
android:foregroundServiceType="microphone"
to your service declaration inAndroidManifest.xml
. - Add
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
to your permissions list inAndroidManifest.xml
. - Add
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
to the flags when callingstartForeground
.
Note that there are also things that I've found don't matter:
- It doesn't matter if you create the thread and/or AudioRecord "from" the foreground-service; the foreground-service (with valid flags and such, as seen above) seems to merely need to be alive at the time the audio is trying to be recorded/read. (ie. in my working app, the foreground-service part of the code doesn't actually interact with the microphone-related code, I just need to make sure the service is active at the time)
- The call to
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
in the original gist also appears to not be needed. (I removed it without noticing any ill effect)
Side note: There apparently is a different way to record with the screen-off, even without the changes above. In my app, it seems like what did the trick was triggering the recording-thread-creation and record-starting from the VolumeProvider.onAdjustVolume
handler in my foreground service, at a point when the screen was already off.
However, I didn't like how the user had to manually press a button after the screen was off to get it started. So I did end up taking the standard route, outlined earlier. (it's probably better that way anyway; but I figured I would note that there does seem to be some cases where you can record with screen-off, without those extra permissions, though I didn't take the time to work out the exact criteria for it)
@Venryx can I hire you to solve such issue in my project? How I can contact you? If you are interested please write me on https://www.linkedin.com/in/maksym-maslakov/
@Venryx can I hire you to solve such issue in my project? How I can contact you? If you are interested please write me on https://www.linkedin.com/in/maksym-maslakov/
Thanks for the offer, but I don't have time/availability for that right now.
@bayerlse
@rayyan808
using the foreground service is a solution I think.
More on that here #react-native-webrtc/react-native-callkeep#274