-
-
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(); | |
| } | |
| } |
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_MICROPHONEto 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.
Has anyone tried this working code?