I'm trying to write a CallRecorder app in Kotlin. What I'm trying to do is to start recording the audio with a Service that is launched by a BroadcastReceiver. I think I'm doing something wrong with the MediaRecorder initialization, but I can't figure what.
EDIT: Found and resolved the MediaRecorder problem (changing the file name from "dd.MMM.yyyy-HH:mm:ss" to "dd.MMM.yyyy". Android didn't like the colon in the file name). Now it gives me a "FATAL EXCEPTION" about the IntentService. Look in the updated stack-trace below.
Manifest:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<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=".RecordService"
android:exported="false"/>
<receiver android:name=".CallReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE"/>
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Main Activity:
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, ActivityCompat.OnRequestPermissionsResultCallback {
private val READ_PHONE_STATE : String = Manifest.permission.READ_PHONE_STATE
private val RECORD_AUDIO : String = Manifest.permission.RECORD_AUDIO
private val WRITE_EXTERNAL_STORAGE : String = Manifest.permission.WRITE_EXTERNAL_STORAGE
private val PERMISSION_LIST = arrayOf(READ_PHONE_STATE, RECORD_AUDIO, WRITE_EXTERNAL_STORAGE)
private val REQUEST_CODE : Int = 101
private val PERMISSION_GRANTED : Int = PackageManager.PERMISSION_GRANTED
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
/*fab.setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}*/
checkPermissions()
val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawer_layout.addDrawerListener(toggle)
toggle.syncState()
nav_view.setNavigationItemSelectedListener(this)
}
fun checkPermissions(){
var tempPermissionList : Array<String?> = arrayOfNulls(3)
var position : Int = 0
for(permission in PERMISSION_LIST){
if(ContextCompat.checkSelfPermission(this, permission) != PERMISSION_GRANTED) tempPermissionList.set(position, permission)
position++
}
if(!tempPermissionList.isEmpty()){
ActivityCompat.requestPermissions(this, tempPermissionList, REQUEST_CODE)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if(requestCode == REQUEST_CODE) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onBackPressed() {
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
drawer_layout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) {
R.id.action_settings -> return true
else -> return super.onOptionsItemSelected(item)
}
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Handle navigation view item clicks here.
when (item.itemId) {
R.id.nav_camera -> {
// Handle the camera action
}
R.id.nav_gallery -> {
}
R.id.nav_slideshow -> {
}
R.id.nav_manage -> {
}
R.id.nav_share -> {
}
R.id.nav_send -> {
}
}
drawer_layout.closeDrawer(GravityCompat.START)
return true
}
}
Recorder:
class Recorder {
val TAG : String = "RECORDER"
var mediaRecorder : MediaRecorder? = null
fun startRecording(){
if(mediaRecorder != null){
mediaRecorder!!.stop()
mediaRecorder!!.reset()
mediaRecorder!!.release()
mediaRecorder = null
}
mediaRecorder = MediaRecorder()
mediaRecorder!!.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL)
mediaRecorder!!.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
mediaRecorder!!.setOutputFile(generateFilePath())
mediaRecorder!!.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_WB)
try {
mediaRecorder!!.prepare()
} catch (e:Exception){
Log.e(TAG, "prepare() failed")
}
mediaRecorder!!.start()
}
fun stopRecording(){
if(mediaRecorder != null){
mediaRecorder!!.stop()
mediaRecorder!!.release()
mediaRecorder = null
}
}
private fun getCurrentDate() : String{
val calendar = java.util.Calendar.getInstance()
val dateFormat = SimpleDateFormat("dd.MMM.yyyy")
val date : String = dateFormat.format(calendar.time)
return date
}
private fun generateFilePath() : String{
return Environment.getExternalStorageDirectory().absolutePath + "/" + getCurrentDate() + ".3gp"
}
}
CallReceiver:
class CallReceiver : BroadcastReceiver() {
private val TAG = "CALL_RECEIVER"
private val PHONE_STATE : String = "PHONE_STATE"
private val START_RECORDING : String = "START_RECORDING"
private val STOP_RECORDING : String = "STOP_RECORDING"
//private lateinit var mRecorder : Recorder
override fun onReceive(context: Context?, intent: Intent?) {
val phoneState : String? = intent?.getStringExtra(TelephonyManager.EXTRA_STATE)
if(phoneState.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)){
Toast.makeText(context, "REGISTRAZIONE INIZIATA", Toast.LENGTH_SHORT).show()
Log.d(TAG, "START RECORDING")
var intent2 = Intent(context, RecordService::class.java)
intent2.putExtra(PHONE_STATE, START_RECORDING)
context?.startService(intent2)
//if(mRecorder == null) mRecorder = Recorder()
//mRecorder.startRecording()
} else if (phoneState.equals(TelephonyManager.EXTRA_STATE_IDLE)){
Toast.makeText(context, "REGISTRAZIONE TERMINATA", Toast.LENGTH_SHORT).show()
Log.d(TAG, "STOP RECORDING")
var intent3 = Intent(context, RecordService::class.java)
intent3.putExtra(PHONE_STATE, STOP_RECORDING)
context?.startService(intent3)
//mRecorder.stopRecording()
}
}
}
RecordService:
class RecordService : IntentService("RecordService") {
private val PHONE_STATE : String = "PHONE_STATE"
private val START_RECORDING : String = "START_RECORDING"
private val STOP_RECORDING : String = "STOP_RECORDING"
private lateinit var mRecorder : Recorder
override fun onCreate() {
mRecorder = Recorder()
super.onCreate()
}
override fun onHandleIntent(intent: Intent?) {
when(intent?.getStringExtra(PHONE_STATE)){
START_RECORDING -> mRecorder.startRecording()
STOP_RECORDING -> mRecorder.stopRecording()
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
}
Stack trace:
06-25 19:11:12.889 9104-9104/com.example.luca.kallrecorder D/CALL_RECEIVER: START RECORDING
06-25 19:11:12.897 9104-9104/com.example.luca.kallrecorder D/CALL_RECEIVER: START RECORDING
06-25 19:11:12.928 9104-9123/com.example.luca.kallrecorder D/EGL_emulation: eglMakeCurrent: 0xae834c40: ver 2 0
06-25 19:11:12.930 9104-9139/com.example.luca.kallrecorder E/MediaRecorder: start failed: -2147483648
06-25 19:11:12.931 9104-9139/com.example.luca.kallrecorder E/AndroidRuntime: FATAL EXCEPTION: IntentService[RecordService]
Process: com.example.luca.kallrecorder, PID: 9104
java.lang.RuntimeException: start failed.
at android.media.MediaRecorder.start(Native Method)
at com.example.luca.kallrecorder.Recorder.startRecording(Recorder.kt:33)
at com.example.luca.kallrecorder.RecordService.onHandleIntent(RecordService.kt:25)
at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:65)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.os.HandlerThread.run(HandlerThread.java:61)
06-25 19:11:12.952 9104-9123/com.example.luca.kallrecorder V/RenderScript: 0xae8eb800 Launching thread(s), CPUs 4
06-25 19:11:14.891 9104-9123/com.example.luca.kallrecorder D/EGL_emulation: eglMakeCurrent: 0xae834c40: ver 2 0
06-25 19:11:14.929 9104-9123/com.example.luca.kallrecorder D/EGL_emulation: eglMakeCurrent: 0xae834c40: ver 2 0
As suggested by Mike M. in the comments, I resolved by implementing Service instead of IntentService. I quote what Mike M. said: "IntentService... will stop itself pretty much immediately after onHandleIntent() finishes each time.". I suppose that MediaRecorder instance was disappearing as soon as the system lost its reference to onHandleIntent().
My app doesn't work as desired yet, but it's not because of that error. Actually it compiles and runs without any error right now.
This is the updated RecordService class, implementing Service:
class RecordService : Service() {
private val PHONE_STATE : String = "PHONE_STATE"
private val START_RECORDING : String = "START_RECORDING"
private val STOP_RECORDING : String = "STOP_RECORDING"
private var mRecorder : Recorder? = null
private lateinit var mServiceLooper : Looper
override fun onCreate() {
mRecorder = Recorder()
super.onCreate()
}
override fun onBind(intent: Intent?): IBinder {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when(intent?.getStringExtra(PHONE_STATE)){
START_RECORDING -> thread { Runnable { kotlin.run { mRecorder!!.stopRecording() } } }
STOP_RECORDING -> thread { Runnable { kotlin.run { mRecorder!!.stopRecording() } } }
}
return super.onStartCommand(intent, flags, startId)
}
}