-
-
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(); | |
} | |
} |
Unfortunately, I've never tried to record from the internal audio, so I don't know if it can be done with fewer permissions. Good luck though!
// However, to be on the safe side you might want to activate them anyway.
hey where to stop and stop that power lock???
Seems not to work with Android 11, right?
Seems not to work with Android 11, right?
Sorry, but I have no idea; I haven't done any significant Android development for about a year now, and do not have an Android 11 device.
@bayerlse did you find any solution on android 11 ? i am also facing following issue
While user press home button or screen lock button mediaRecorder stop recording voice but keep writing bytes to file.
@bayerlse did you find any solution on android 11 ? i am also facing following issue While user press home button or screen lock button mediaRecorder stop recording voice but keep writing bytes to file.
Same here, have you come across any fix? I'm unable to get mediaRecorder to consistently record after the screen is off.
@bayerlse
@rayyan808
using the foreground service is a solution I think.
More on that here #react-native-webrtc/react-native-callkeep#274
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.
Dear Stephen,
congratulations for such a great code.
Given your obvious experience, I would like, if you allow me, to ask you for some suggestions.
I'm not an expert so forgive any silly questions.
Using the microphone requires explicit permission, of course. However, I have read that internal audio could be recorded without permissions. The question is: do you think it is possible to by-pass the permissions regarding the use of the microphone, perhaps finding a way to redirect such an audio to the internal audio, or similar solutions?
Thanks a lot, and congratulations again.
Rocco Z.