308 lines
10 KiB
Kotlin
308 lines
10 KiB
Kotlin
package com.simplemobiletools.voicerecorder.services
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.annotation.TargetApi
|
|
import android.app.*
|
|
import android.content.ContentValues
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.media.MediaScannerConnection
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.IBinder
|
|
import android.provider.MediaStore
|
|
import android.provider.MediaStore.Audio.Media
|
|
import androidx.core.app.NotificationCompat
|
|
import com.simplemobiletools.commons.extensions.*
|
|
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
|
import com.simplemobiletools.commons.helpers.isOreoPlus
|
|
import com.simplemobiletools.commons.helpers.isRPlus
|
|
import com.simplemobiletools.voicerecorder.R
|
|
import com.simplemobiletools.voicerecorder.activities.SplashActivity
|
|
import com.simplemobiletools.voicerecorder.extensions.config
|
|
import com.simplemobiletools.voicerecorder.extensions.getDefaultRecordingsRelativePath
|
|
import com.simplemobiletools.voicerecorder.extensions.updateWidgets
|
|
import com.simplemobiletools.voicerecorder.helpers.*
|
|
import com.simplemobiletools.voicerecorder.models.Events
|
|
import com.simplemobiletools.voicerecorder.recorder.MediaRecorderWrapper
|
|
import com.simplemobiletools.voicerecorder.recorder.Mp3Recorder
|
|
import com.simplemobiletools.voicerecorder.recorder.Recorder
|
|
import org.greenrobot.eventbus.EventBus
|
|
import java.io.File
|
|
import java.util.*
|
|
import com.simplemobiletools.commons.R as CommonsR
|
|
|
|
class RecorderService : Service() {
|
|
companion object {
|
|
var isRunning = false
|
|
}
|
|
|
|
private val AMPLITUDE_UPDATE_MS = 75L
|
|
|
|
private var currFilePath = ""
|
|
private var duration = 0
|
|
private var status = RECORDING_STOPPED
|
|
private var durationTimer = Timer()
|
|
private var amplitudeTimer = Timer()
|
|
private var recorder: Recorder? = null
|
|
|
|
override fun onBind(intent: Intent?): IBinder? = null
|
|
|
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
|
super.onStartCommand(intent, flags, startId)
|
|
|
|
when (intent.action) {
|
|
GET_RECORDER_INFO -> broadcastRecorderInfo()
|
|
STOP_AMPLITUDE_UPDATE -> amplitudeTimer.cancel()
|
|
TOGGLE_PAUSE -> togglePause()
|
|
else -> startRecording()
|
|
}
|
|
|
|
return START_NOT_STICKY
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
stopRecording()
|
|
isRunning = false
|
|
updateWidgets(false)
|
|
}
|
|
|
|
// mp4 output format with aac encoding should produce good enough m4a files according to https://stackoverflow.com/a/33054794/1967672
|
|
private fun startRecording() {
|
|
isRunning = true
|
|
updateWidgets(true)
|
|
if (status == RECORDING_RUNNING) {
|
|
return
|
|
}
|
|
|
|
val defaultFolder = File(config.saveRecordingsFolder)
|
|
if (!defaultFolder.exists()) {
|
|
defaultFolder.mkdir()
|
|
}
|
|
|
|
val baseFolder = if (isRPlus() && !hasProperStoredFirstParentUri(defaultFolder.absolutePath)) {
|
|
cacheDir
|
|
} else {
|
|
defaultFolder.absolutePath
|
|
}
|
|
|
|
currFilePath = "$baseFolder/${getCurrentFormattedDateTime()}.${config.getExtension()}"
|
|
|
|
try {
|
|
recorder = if (recordMp3()) {
|
|
Mp3Recorder(this)
|
|
} else {
|
|
MediaRecorderWrapper(this)
|
|
}
|
|
|
|
if (isRPlus() && hasProperStoredFirstParentUri(currFilePath)) {
|
|
val fileUri = createDocumentUriUsingFirstParentTreeUri(currFilePath)
|
|
createSAFFileSdk30(currFilePath)
|
|
val outputFileDescriptor = contentResolver.openFileDescriptor(fileUri, "w")!!.fileDescriptor
|
|
recorder?.setOutputFile(outputFileDescriptor)
|
|
} else if (!isRPlus() && isPathOnSD(currFilePath)) {
|
|
var document = getDocumentFile(currFilePath.getParentPath())
|
|
document = document?.createFile("", currFilePath.getFilenameFromPath())
|
|
|
|
val outputFileDescriptor = contentResolver.openFileDescriptor(document!!.uri, "w")!!.fileDescriptor
|
|
recorder?.setOutputFile(outputFileDescriptor)
|
|
} else {
|
|
recorder?.setOutputFile(currFilePath)
|
|
}
|
|
|
|
recorder?.prepare()
|
|
recorder?.start()
|
|
duration = 0
|
|
status = RECORDING_RUNNING
|
|
broadcastRecorderInfo()
|
|
startForeground(RECORDER_RUNNING_NOTIF_ID, showNotification())
|
|
|
|
durationTimer = Timer()
|
|
durationTimer.scheduleAtFixedRate(getDurationUpdateTask(), 1000, 1000)
|
|
|
|
startAmplitudeUpdates()
|
|
} catch (e: Exception) {
|
|
showErrorToast(e)
|
|
stopRecording()
|
|
}
|
|
}
|
|
|
|
private fun stopRecording() {
|
|
durationTimer.cancel()
|
|
amplitudeTimer.cancel()
|
|
status = RECORDING_STOPPED
|
|
|
|
recorder?.apply {
|
|
try {
|
|
stop()
|
|
release()
|
|
|
|
ensureBackgroundThread {
|
|
if (isRPlus() && !hasProperStoredFirstParentUri(currFilePath)) {
|
|
addFileInNewMediaStore()
|
|
} else {
|
|
addFileInLegacyMediaStore()
|
|
}
|
|
EventBus.getDefault().post(Events.RecordingCompleted())
|
|
}
|
|
} catch (e: Exception) {
|
|
showErrorToast(e)
|
|
}
|
|
}
|
|
recorder = null
|
|
}
|
|
|
|
private fun broadcastRecorderInfo() {
|
|
broadcastDuration()
|
|
broadcastStatus()
|
|
startAmplitudeUpdates()
|
|
}
|
|
|
|
private fun startAmplitudeUpdates() {
|
|
amplitudeTimer.cancel()
|
|
amplitudeTimer = Timer()
|
|
amplitudeTimer.scheduleAtFixedRate(getAmplitudeUpdateTask(), 0, AMPLITUDE_UPDATE_MS)
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
private fun togglePause() {
|
|
try {
|
|
if (status == RECORDING_RUNNING) {
|
|
recorder?.pause()
|
|
status = RECORDING_PAUSED
|
|
} else if (status == RECORDING_PAUSED) {
|
|
recorder?.resume()
|
|
status = RECORDING_RUNNING
|
|
}
|
|
broadcastStatus()
|
|
startForeground(RECORDER_RUNNING_NOTIF_ID, showNotification())
|
|
} catch (e: Exception) {
|
|
showErrorToast(e)
|
|
}
|
|
}
|
|
|
|
@SuppressLint("InlinedApi")
|
|
private fun addFileInNewMediaStore() {
|
|
val audioCollection = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
val storeFilename = currFilePath.getFilenameFromPath()
|
|
|
|
val newSongDetails = ContentValues().apply {
|
|
put(Media.DISPLAY_NAME, storeFilename)
|
|
put(Media.TITLE, storeFilename)
|
|
put(Media.MIME_TYPE, storeFilename.getMimeType())
|
|
put(Media.RELATIVE_PATH, getDefaultRecordingsRelativePath())
|
|
}
|
|
|
|
val newUri = contentResolver.insert(audioCollection, newSongDetails)
|
|
if (newUri == null) {
|
|
toast(CommonsR.string.unknown_error_occurred)
|
|
return
|
|
}
|
|
|
|
try {
|
|
val outputStream = contentResolver.openOutputStream(newUri)
|
|
val inputStream = getFileInputStreamSync(currFilePath)
|
|
inputStream!!.copyTo(outputStream!!, DEFAULT_BUFFER_SIZE)
|
|
recordingSavedSuccessfully(newUri)
|
|
} catch (e: Exception) {
|
|
showErrorToast(e)
|
|
}
|
|
}
|
|
|
|
private fun addFileInLegacyMediaStore() {
|
|
MediaScannerConnection.scanFile(
|
|
this,
|
|
arrayOf(currFilePath),
|
|
arrayOf(currFilePath.getMimeType())
|
|
) { _, uri -> recordingSavedSuccessfully(uri) }
|
|
}
|
|
|
|
private fun recordingSavedSuccessfully(savedUri: Uri) {
|
|
toast(R.string.recording_saved_successfully)
|
|
EventBus.getDefault().post(Events.RecordingSaved(savedUri))
|
|
}
|
|
|
|
private fun getDurationUpdateTask() = object : TimerTask() {
|
|
override fun run() {
|
|
if (status == RECORDING_RUNNING) {
|
|
duration++
|
|
broadcastDuration()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getAmplitudeUpdateTask() = object : TimerTask() {
|
|
override fun run() {
|
|
if (recorder != null) {
|
|
try {
|
|
EventBus.getDefault().post(Events.RecordingAmplitude(recorder!!.getMaxAmplitude()))
|
|
} catch (ignored: Exception) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.O)
|
|
private fun showNotification(): Notification {
|
|
val hideNotification = config.hideNotification
|
|
val channelId = "simple_recorder"
|
|
val label = getString(R.string.app_name)
|
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
if (isOreoPlus()) {
|
|
val importance = if (hideNotification) NotificationManager.IMPORTANCE_MIN else NotificationManager.IMPORTANCE_DEFAULT
|
|
NotificationChannel(channelId, label, importance).apply {
|
|
setSound(null, null)
|
|
notificationManager.createNotificationChannel(this)
|
|
}
|
|
}
|
|
|
|
var priority = Notification.PRIORITY_DEFAULT
|
|
var icon = CommonsR.drawable.ic_microphone_vector
|
|
var title = label
|
|
var visibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
var text = getString(R.string.recording)
|
|
if (status == RECORDING_PAUSED) {
|
|
text += " (${getString(R.string.paused)})"
|
|
}
|
|
|
|
if (hideNotification) {
|
|
priority = Notification.PRIORITY_MIN
|
|
icon = R.drawable.ic_empty
|
|
title = ""
|
|
text = ""
|
|
visibility = NotificationCompat.VISIBILITY_SECRET
|
|
}
|
|
|
|
val builder = NotificationCompat.Builder(this, channelId)
|
|
.setContentTitle(title)
|
|
.setContentText(text)
|
|
.setSmallIcon(icon)
|
|
.setContentIntent(getOpenAppIntent())
|
|
.setPriority(priority)
|
|
.setVisibility(visibility)
|
|
.setSound(null)
|
|
.setOngoing(true)
|
|
.setAutoCancel(true)
|
|
|
|
return builder.build()
|
|
}
|
|
|
|
private fun getOpenAppIntent(): PendingIntent {
|
|
val intent = getLaunchIntent() ?: Intent(this, SplashActivity::class.java)
|
|
return PendingIntent.getActivity(this, RECORDER_RUNNING_NOTIF_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
}
|
|
|
|
private fun broadcastDuration() {
|
|
EventBus.getDefault().post(Events.RecordingDuration(duration))
|
|
}
|
|
|
|
private fun broadcastStatus() {
|
|
EventBus.getDefault().post(Events.RecordingStatus(status))
|
|
}
|
|
|
|
private fun recordMp3(): Boolean {
|
|
return config.extension == EXTENSION_MP3
|
|
}
|
|
}
|