Поиск по этому блогу

пятница, 19 июля 2019 г.

Запись экрана с помощью фонового сервиса.

Бывют такие не типичные задачи, когда нужно на пример записывать экран смартфона, то что там происходит, паузить, резюмить и останавливать запись. Записывать это все в видео файл, и дальше уже выполнять над ним какие-то действия.

Данный функционал требует нескольких этапов для старта записи. Первый этап — это старт сервиса который проверяет есть ли перманентный доступ на запись, если его нету, то мы переходим ко второму этапу — это запрашиваем доступ на запись, если пользователь дает доступ на запись, тогда мы переходим на третий этап — мы начинаем запись. Это такой небольшой алгоритм который будет при вызове метода startRecording.

Наше приложение будет представлять из себя один экран с кнопкой по нажатию на которую мы будем запускать наш сервис по записи экрана, после повторной записи мы будем останавливать запись и отображать нотфикейшн который будет по клику запускать видео что бы его можно было просмотреть.

image

Наш сервис будет сам по себе отдельным модулем который можно просто перенести в любой другой проект, и он будет независимо выполнять свою функцию. Для начала нам нужно будет создать сервис который мы будем вызывать из хелпера, котороый в свою очередь будет вызывать диалог с вопросом можно ли записывать экран, и после получения разрешения начнет записывать. Так же можно расширить функционал и добавить сервис с плавающей кнопкой по нажатию на которую будет останавливаться запись или какие-то другие действия.

Вот такой у нас будет флоу действия. Теперь можно приступать к разработке. И начнем мы с сервиса который будет производить запись экрана. Но для начала нам нужно создать несколько листенеров которые будут кидать колбеки в хелперов и получать их обратно.


Давайте посмотрим какаие библиотеки мы будем использовать в gradle файле.

app/build.gradle
dependencies {
    implementation 'androidx.appcompat:appcompat:1.0.2'

    implementation 'com.karumi:dexter:5.0.0'
}
Всего лишь два, как оказалось, ой как же это удивительно. 
— appcompat — нужен для использования всех прелестей androidX.
— dexter — для того что бы спросить пользователя дать пермишены на запись экрана и запись и тение файлов.

Дальше нам нужно будет создать пару листенеров которые нам будут нужны чуть папизже.

RecordingPermissionListener.kt
interface RecordingPermissionListener {
    fun onPermissionGranted()

    fun onPermissionDenied()
}

RecordListener.kt
interface RecordListener {
    fun onStartRecording()

    fun onStopRecording()
}

StopRecordingListener.kt
interface StopRecordingListener {
    fun onStopRecording()

    fun onSystemStopRecording()
}
Эти три интерфейса будут встречаться нам очень часто в этой статье, они будут использоваться почти в каждом классе который относится к записи экрана. Они будут передавать в наши хелперы и менеджеры нужные состояния.

RecordService.kt
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import dajver.com.recordscreenexample.recorder.activity.RequestMediaProjectionActivity
import dajver.com.recordscreenexample.recorder.helpers.ScreenRecorderHelper
import dajver.com.recordscreenexample.recorder.service.listeners.RecordListener
import dajver.com.recordscreenexample.recorder.service.listeners.RecordingPermissionListener
import dajver.com.recordscreenexample.recorder.service.listeners.StopRecordingListener

import java.io.File
import java.io.IOException

class RecordService : Service(), RecordListener {

    private var mStartRecordingListener: RecordingPermissionListener? = null
    private var mStopRecordingListener: StopRecordingListener? = null
    private var mSystemStopRecordingListener: StopRecordingListener? = null
    private var mScreenRecorderHelper: ScreenRecorderHelper? = null

    private val mIBinder = RecordServiceBinder()

    private val isRecording: Boolean get() = mScreenRecorderHelper!!.isRecording

    override fun onCreate() {
        super.onCreate()
        mScreenRecorderHelper = ScreenRecorderHelper(this)
        mScreenRecorderHelper!!.setRecordListener(this)
    }

    override fun onDestroy() {
        mScreenRecorderHelper!!.setRecordListener(null)
        mScreenRecorderHelper!!.release()
        super.onDestroy()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (intent != null && intent.hasExtra(ACTION_CODE_EXTRA_DATA)) {
            when (intent.getIntExtra(ACTION_CODE_EXTRA_DATA, -1)) {
                ACTION_START_RECORDING -> onActionStartRecording(intent)
                ACTION_STOP_RECORDING -> onActionStopRecording()
            }
        }
        return START_STICKY
    }

    override fun onBind(intent: Intent): IBinder? {
        return mIBinder
    }

    override fun onUnbind(intent: Intent): Boolean {
        stopThis()
        return super.onUnbind(intent)
    }

    override fun onStartRecording() {
        if (mStartRecordingListener != null)
            mStartRecordingListener!!.onPermissionGranted()
    }

    override fun onStopRecording() {
        if (mStopRecordingListener != null) {
            mStopRecordingListener!!.onStopRecording()
            stopThis()
        } else if (mSystemStopRecordingListener != null) {
            mSystemStopRecordingListener!!.onSystemStopRecording()
            stopThis()
        }
    }

    private fun onActionStartRecording(intent: Intent) {
        if (!intent.hasExtra(OUTPUT_FILE_PATH_EXTRA_DATA)) {
            return
        }

        if (intent.hasExtra(STATE_RESULT_CODE) && intent.hasExtra(STATE_RESULT_DATA)) {
            val resultCode = intent.getIntExtra(STATE_RESULT_CODE, 0)
            val resultData = intent.getParcelableExtra<Intent>(STATE_RESULT_DATA)

            // Checking the action from the user, Granted or Denied permission
            if (!mScreenRecorderHelper!!.onScreenRecordPermissionResult(resultCode, resultData)) {
                if (mStartRecordingListener != null)
                    mStartRecordingListener!!.onPermissionDenied()

                stopThis()

                return
            }
        }

        startRecording(intent.getStringExtra(OUTPUT_FILE_PATH_EXTRA_DATA))
    }

    private fun onActionStopRecording() {
        stopRecording()
    }

    private fun startRecording(outputFilePath: String?) {
        if (hasScreenRecordPermission()) {
            try {
                mScreenRecorderHelper!!.startRecording(File(outputFilePath!!))
            } catch (e: IOException) {
                e.printStackTrace()
                stopThis()
            } catch (ignore: IllegalStateException) { }
        } else {
            // Requesting media projection
            requestPermission(outputFilePath)
        }
    }

    private fun stopRecording() {
        mScreenRecorderHelper!!.stopRecording()
    }

    private fun hasScreenRecordPermission(): Boolean {
        return mScreenRecorderHelper!!.hasScreenRecordPermission()
    }

    private fun requestPermission(outputFilePath: String?) {
        // Requesting media projection
        startActivity(RequestMediaProjectionActivity.createIntent(this@RecordService, ACTION_START_RECORDING, outputFilePath!!))
    }

    private fun stopThis() {
        stopForeground(true)
        stopSelf()
    }

    inner class RecordServiceBinder : Binder() {
        fun startRecording(outputFilePath: String, startRecordingListener: RecordingPermissionListener, stopCurrentIfRecording: Boolean, systemStopRecordingListener: StopRecordingListener) {
            if (stopCurrentIfRecording && this@RecordService.isRecording) {
                this@RecordService.stopRecording()
            }

            mStartRecordingListener = startRecordingListener
            mSystemStopRecordingListener = systemStopRecordingListener
            this@RecordService.startRecording(outputFilePath)
        }

        fun stopRecording(listener: StopRecordingListener) {
            mStopRecordingListener = listener
            this@RecordService.stopRecording()
        }
    }

    companion object {

        private const val STATE_RESULT_CODE = "result_code"
        private const val STATE_RESULT_DATA = "result_data"

        private const val OUTPUT_FILE_PATH_EXTRA_DATA = "output_file_path"
        private const val ACTION_CODE_EXTRA_DATA = "action_code"

        const val ACTION_START_RECORDING = 0
        const val ACTION_STOP_RECORDING = 1

        fun createIntent(context: Context, actionCode: Int, outputFilePath: String, resultCode: Int, resultData: Intent): Intent {
            val intent = Intent(context, RecordService::class.java)
            intent.putExtra(ACTION_CODE_EXTRA_DATA, actionCode)
            intent.putExtra(OUTPUT_FILE_PATH_EXTRA_DATA, outputFilePath)
            intent.putExtra(STATE_RESULT_CODE, resultCode)
            intent.putExtra(STATE_RESULT_DATA, resultData)
            return intent
        }
    }
}
Данный сервис довольно типичный. Нас интересует в данном сервисе один класс и один метод.
— onStartCommand — выполняет запуск старта записи и остановки записи.
— RecordServiceBinder — класс через который будет выполнятся запуск или останвока записи, его мы будем вызывать из другого класса-хелпера который будет оборачивать в себе все нужные функции для старта и стопа записи.
Остальные методы выполняют функционал создания записи или остановки, вызов диалога с запросом на запись и так далее… Названия говорят сами за себя.

Далее нам нужно создать активити которая будет показывать диалог с вопросом можно ли записывать.

RequestMediaProjectionActivity.kt
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjectionManager
import androidx.appcompat.app.AppCompatActivity
import dajver.com.recordscreenexample.recorder.service.RecordService

class RequestMediaProjectionActivity : AppCompatActivity() {

    private var mRequestMediaProjection = true

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == REQUEST_MEDIA_PROJECTION_CODE) {
            mRequestMediaProjection = false

            startService(RecordService.createIntent(this,
                    intent.getIntExtra(ACTION_CODE_EXTRA_DATA, -1),
                    intent.getStringExtra(OUTPUT_FILE_PATH_EXTRA_DATA)!!,
                    resultCode, data!!
                )
            )

            finishAndRemoveTask()
        }
        super.onActivityResult(requestCode, resultCode, data)
    }

    override fun onResume() {
        super.onResume()
        if (mRequestMediaProjection) {
            requestScreenRecordPermission()
        } else {
            finishAndRemoveTask()
        }
    }

    private fun requestScreenRecordPermission() {
        val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        if (mediaProjectionManager != null) {
            startActivityForResult(
                mediaProjectionManager.createScreenCaptureIntent(),
                REQUEST_MEDIA_PROJECTION_CODE
            )
        }
    }

    companion object {

        private const val REQUEST_MEDIA_PROJECTION_CODE = 613

        private const val ACTION_CODE_EXTRA_DATA = "action_code"
        private const val OUTPUT_FILE_PATH_EXTRA_DATA = "output_file_path"

        fun createIntent(context: Context, actionCode: Int, outputFilePath: String): Intent {
            val intent = Intent(context, RequestMediaProjectionActivity::class.java)

            intent.putExtra(ACTION_CODE_EXTRA_DATA, actionCode)
            intent.putExtra(OUTPUT_FILE_PATH_EXTRA_DATA, outputFilePath)

            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

            return intent
        }
    }
}
Здесь нас интересует так же пара методов, остальные нуны чисто что бы эти пара методов жили так как мы хотим.
— createIntent — метод который вызывает RequestMediaProjectionActivity и задает параметры по которым мы дальше будем создавать видео-файл и передаем код для onActivityResult
— onActivityResult в свою очередь запускает сервис через метод который задает путь файла для записи.
— requestScreenRecordPermission — запрашивает разрашение у пользователя на запись экрана.

Далее нам нужно создать класс который будет создавать инстанс MediaRecorder и работать с этим инстансом для создания собственно видео-файла и последующей записи в него.

State.kt
enum class State {
    INITIAL, RECORDING
}
Но для начала нам нужно создать енам с нужными нам параметрами для сохранения стейтов записи. Создали, а дальше уже смотрим что из себя представляет наш ScreenRecorder.

ScreenRecorder.kt
import android.graphics.Point
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.MediaRecorder
import android.media.projection.MediaProjection
import android.util.SparseIntArray
import android.view.Surface
import android.view.WindowManager
import dajver.com.recordscreenexample.recorder.enums.State

import java.io.File
import java.io.IOException

class ScreenRecorder internal constructor(private val mWindowManager: WindowManager, private val mScreenDensity: Int) {

    private val mScreenSize = Point()

    private var mOutputFile: File? = null

    private var mVirtualDisplay: VirtualDisplay? = null
    private var mMediaRecorder: MediaRecorder? = null

    private var mDisplayWidth: Int = 0
    private var mDisplayHeight: Int = 0
    private var mVideoBitRate: Int = 0
    private var mVideoFrameRate: Int = 0

    private var mState = State.INITIAL

    val isRecording: Boolean get() = mState == State.RECORDING

    private val displayHeight: Int get() = if (mScreenSize.y == 0) mDisplayHeight else mScreenSize.y

    private val screenWidth: Int get() = if (mScreenSize.x == 0) mDisplayWidth else mScreenSize.x

    init {
        mWindowManager.defaultDisplay.getSize(mScreenSize)
        setupVideoQualitySettings()
    }

    @Throws(IOException::class)
    internal fun startRecord(mediaProjection: MediaProjection, outputFile: File) {
        // Checking the arguments

        mOutputFile = outputFile

        // Checking the file
        if (mOutputFile!!.isDirectory) {
            throw IllegalArgumentException("It is a directory not a file!")
        }

        if (mState != State.INITIAL) {
            throw IllegalStateException("You can start recording only on the initial state!")
        }

        // Set up media recorder
        mMediaRecorder = MediaRecorder()
        initMediaRecorder()

        // Sharing the screen
        shareScreen(mediaProjection)

        mState = State.RECORDING
    }

    internal fun stopRecord() {
        if (mState != State.RECORDING) {
            return
        }

        if (mMediaRecorder == null)
            return

        try {
            mMediaRecorder!!.stop()
        } catch (ignore: RuntimeException) { }

        stopScreenSharing()
        mMediaRecorder!!.reset()
        mMediaRecorder!!.release()
        mMediaRecorder = null

        mState = State.INITIAL
    }

    @Throws(IOException::class)
    private fun initMediaRecorder() {
        mMediaRecorder!!.setVideoSource(MediaRecorder.VideoSource.SURFACE)
        mMediaRecorder!!.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        mMediaRecorder!!.setOutputFile(mOutputFile!!.path)
        mMediaRecorder!!.setVideoSize(mDisplayWidth, mDisplayHeight)
        mMediaRecorder!!.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
        mMediaRecorder!!.setVideoEncodingBitRate(mVideoBitRate)
        mMediaRecorder!!.setVideoFrameRate(mVideoFrameRate)

        val rotation = mWindowManager.defaultDisplay.rotation
        val orientation = ORIENTATIONS.get(rotation + 90)
        mMediaRecorder!!.setOrientationHint(orientation)

        mMediaRecorder!!.prepare()
    }

    private fun setupVideoQualitySettings() {
        mDisplayWidth = 1280
        mDisplayHeight = 720

        mVideoBitRate = 1600000
        mVideoFrameRate = 24
    }

    private fun setUpVirtualDisplay(mediaProjection: MediaProjection) {
        mVirtualDisplay = mediaProjection.createVirtualDisplay("ScreenRecorder",
            mDisplayWidth, mDisplayHeight, mScreenDensity,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            mMediaRecorder!!.surface, null, null
        )
    }

    private fun shareScreen(mediaProjection: MediaProjection) {
        try {
            setUpVirtualDisplay(mediaProjection)
            mMediaRecorder!!.start()
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }

    private fun stopScreenSharing() {
        if (mVirtualDisplay != null) {
            mVirtualDisplay!!.release()
            mVirtualDisplay = null
        }
    }

    companion object {

        private val ORIENTATIONS = SparseIntArray()

        init {
            ORIENTATIONS.append(Surface.ROTATION_0, 90)
            ORIENTATIONS.append(Surface.ROTATION_90, 0)
            ORIENTATIONS.append(Surface.ROTATION_180, 270)
            ORIENTATIONS.append(Surface.ROTATION_270, 180)
        }
    }
}
В инициализации мы задаем ширину экрана какую мы хотим записывать, собствнно берем всю ширину экрана возможную и так же указываем качество которое мы хотим что бы было у записанного видео. Видео будет с шириной 1200 пикселей и с высотой в 720 пикселей, битрейт (частоту записи видео) мы задали 1600000 бит / секунду и фрейм рейт (колиество кадров в секунду) мы задали в 24 к / с.
— initMediaRecorder — этот метод у нас инициализирует и задает важные параметры такие как адрес куда будет писаться видео, с каким качеством, битрейтом, фрейм рейтом и так далее…
— startRecord — у нас создает MediaRecorder, инициализирует его и стартует запись экрана. И в конце ставим стейт что у нас видео записывается.
— stopRecord — останавливаем запись, и ставим стейт что у нас рекордер проинициализирован.
— setUpVirtualDisplay — задает ширину и высоту и создает с ней VirtualDisplay который позволит записать экран.
— shareScreen — начинает запись экрана.

Далее мы создадим отдельный хелпер который будет расширять возможности нашего ScreenRecorder'a, так как сам собственно класс рекордера у нас включает в себя инициализацию и старт / стоп записи. А вот в хелпере мы будем запрашивать возможность записи у пользователя и записывать или не записывать видео в зависимости от решения пользователя.

ScreenRecorderHelper.kt 
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.util.DisplayMetrics
import android.view.WindowManager
import dajver.com.recordscreenexample.recorder.service.listeners.RecordListener

import java.io.File
import java.io.IOException

class ScreenRecorderHelper(private val mContext: Context) {

    private var isStartCalled = false

    private var mMediaProjectionManager: MediaProjectionManager? = null
    private var mMediaProjection: MediaProjection? = null
    private var mMediaProjectionCallback: MediaProjectionCallback? = null

    private var mScreenRecorder: ScreenRecorder? = null

    private var mRecordListener: RecordListener? = null

    val isRecording: Boolean get() = screenRecorder.isRecording

    private val screenRecorder: ScreenRecorder
        get() {
            if (mScreenRecorder == null) {
                val windowManager = mContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager

                val metrics = DisplayMetrics()
                windowManager.defaultDisplay.getMetrics(metrics)

                mScreenRecorder = ScreenRecorder(windowManager, metrics.densityDpi)
            }
            return mScreenRecorder!!
        }

    private val mediaProjectionManager: MediaProjectionManager
        get() {
            if (mMediaProjectionManager == null) {
                mMediaProjectionManager = mContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
            }
            return mMediaProjectionManager!!
        }

    fun setRecordListener(listener: RecordListener?) {
        mRecordListener = listener
    }

    fun release() {
        if (screenRecorder.isRecording) {
            stopRecording()
        }
        tearDownMediaProjection()
    }

    @Throws(IOException::class)
    fun startRecording(outputFile: File) {
        if (!isStartCalled)
            isStartCalled = true
        else
            return

        screenRecorder.startRecord(mMediaProjection!!, outputFile)

        mRecordListener!!.onStartRecording()
    }

    fun stopRecording() {
        screenRecorder.stopRecord()

        onStopRecording()
    }

    fun hasScreenRecordPermission(): Boolean {
        return mMediaProjection != null
    }

    fun onScreenRecordPermissionResult(resultCode: Int, resultData: Intent): Boolean {
        return if (resultCode == Activity.RESULT_OK) {
            // Set up media projection
            mMediaProjection = mediaProjectionManager.getMediaProjection(resultCode, resultData)
            mMediaProjectionCallback = MediaProjectionCallback()
            mMediaProjection!!.registerCallback(mMediaProjectionCallback, null)

            true
        } else {
            false
        }
    }

    private fun onStopRecording() {
        isStartCalled = false

        if (mRecordListener != null) {
            mRecordListener!!.onStopRecording()
        }
    }

    private fun tearDownMediaProjection() {
        if (mMediaProjection != null) {
            mMediaProjection!!.unregisterCallback(mMediaProjectionCallback)
            mMediaProjection!!.stop()
            mMediaProjection = null
        }
    }

    private inner class MediaProjectionCallback : MediaProjection.Callback() {
        override fun onStop() {
            if (screenRecorder.isRecording) {
                try {
                    stopRecording()
                } catch (ignore: RuntimeException) {
                    // Handle cleanup here, see:
                    // https://stackoverflow.com/questions/16221866/mediarecorder-failed-when-i-stop-the-recording
                }
            }
            tearDownMediaProjection()
        }
    }
}
Собственно данный класс повторяет функционал ScreenRecorder, но только в этом классе мы добавили запрос с помощью MediaProjectionManager который спрашивает у пользователя «хотиш писать видос или не хотиш?» и пользователь уже решает хотит он или нет. 

Прям все методы я не буду описывать, опишу только пару, в основном данный класс нужен для индикации о том что происходит на экране. Это будут startRecording и stopRecording, два метода которые нам возвращает лисенер из ScreenRecorder, по приходу колбека в эти методы мы отправляем запрос в сервис что бы он проверил все ли ок, хотит пользователь писать или нет, и если хотит — тогда мы пишем в файл, и отправляем колбек в onStartRecording в сервисе который отправляет колбек onPermissionGranted в MainActivity, в которой уже мы будем показывать какой-то индикатор что запись началась. Если же пользователь отказал — то мы отправляем другой колбек onPermissionDenied в MainActivity. И если запись была остановленная то так же отправляется колбек в MainActivity для индикации пользователю.

Все очень сложно и запутанно, я если честно сам офигел, но после пары часов разбора кода я думаю все встанет на свои места у человека которому это нужно :) 

Но это еще не все, у нас еще будет класс менеджер который будет в себе обсорбировать все вызовы сервиса, старт записи, стоп записи и паузы / резюма. Он нам нужен для разделения логики работы что бы вот совсем было просто работать с нашим рекордером, что бы было достаточно создать инстанс RecorderManager и вызвать нужный метод и получить результат, какой-то колбек и еще видео ко всему.

RecorderManager.kt
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import dajver.com.recordscreenexample.recorder.service.RecordService
import dajver.com.recordscreenexample.recorder.service.listeners.RecordingPermissionListener
import dajver.com.recordscreenexample.recorder.service.listeners.StopRecordingListener

import java.io.File

class RecorderManager(private val mContext: Context, private val mRecordingPermissionListener: RecordingPermissionListener, private val mSystemStopRecordingListener: StopRecordingListener) {

    private var mRecordServiceBinder: RecordService.RecordServiceBinder? = null

    private var mIsBound = false
    private var mStartRecordingCalled = false

    private var mFileToUpload: File? = null

    private val mRecordServiceConnection = object : ServiceConnection {

        override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
            mRecordServiceBinder = iBinder as RecordService.RecordServiceBinder
            if (mStartRecordingCalled) {
                mStartRecordingCalled = false
                mRecordServiceBinder!!.startRecording(mFileToUpload!!.absolutePath, mRecordingPermissionListener, true, mSystemStopRecordingListener)
            }
        }

        override fun onServiceDisconnected(componentName: ComponentName) {
            mRecordServiceBinder = null
            mIsBound = false
        }
    }

    fun startRecording(fileToRecord: File) {
        this.mFileToUpload = fileToRecord
        if (mRecordServiceBinder == null) {
            mStartRecordingCalled = true
            bindRecordService()
        } else {
            mRecordServiceBinder!!.startRecording(fileToRecord.absolutePath, mRecordingPermissionListener, true, mSystemStopRecordingListener)
        }
    }

    fun pauseRecording(listener: StopRecordingListener) {
        if (!mStartRecordingCalled) {
            mRecordServiceBinder!!.stopRecording(listener)
        }
    }

    fun resumeRecording(fileToRecord: File) {
        this.mFileToUpload = fileToRecord
        if (mRecordServiceBinder != null) {
            mRecordServiceBinder!!.startRecording(fileToRecord.absolutePath, mRecordingPermissionListener, true, mSystemStopRecordingListener)
        }
    }

    fun stopRecording(listener: StopRecordingListener) {
        mStartRecordingCalled = false

        if(mRecordServiceBinder != null) {
            mRecordServiceBinder!!.stopRecording(listener)
        }

        unbindRecordService()
    }

    fun cancelRecording() {
        mStartRecordingCalled = false

        unbindRecordService()
    }

    private fun bindRecordService() {
        val serviceIntent = Intent(mContext, RecordService::class.java)
        mContext.bindService(serviceIntent, mRecordServiceConnection, Context.BIND_AUTO_CREATE)
        mIsBound = true
    }

    private fun unbindRecordService() {
        if (mIsBound && mRecordServiceBinder != null) {
            mContext.unbindService(mRecordServiceConnection)
            mIsBound = false
        }

        mRecordServiceBinder = null
    }
}
Здесь мы создали инстанс RecordServiceBinder который нам нужен для проверки есть ли у нас на данный момент запись или нет, если RecordServiceBinder = null тогда мы создаем новый, а если не null — то мы запускаем / останавливаем запись.
— onServiceConnected — биндер который запускается сервис запущен и готов записывать, то есть когда мы даем добро на запись экрана.
— bindRecordService — нужен для старта сервиса, мы по сути инициализируем сервис и запускаем запрос на запись видео. Дальше как мы получили какой-то результат, пользователь дал доступ на запись или нет, мы вызываем onServiceConnected который запускает запись.
— unbindRecordService — вызывается в основном когда нам нужно закончить запись и остановить сервис. Его мы будем вызывать в стоп рекординге, когда нам сервис уже не нужен.
— startRecording и stopRecording — проверяют есть ли у нас запись и если нету то создает сервис и начинает ее, и соответственно если запись идет то оно останавливает ее, и все это сопровождается лисенерами которые отправляют колбеки в активити.
— pauseRecording и resumeRecording — останавливает или резюмит запись, по сути тоже самое что и старт стоп, но в случае с паузой и резюмом, нам придется писать в новый файл каждый раз когда мы останавливаем или стартуем запись, дозаписывать в тот же не выйдет.

Далее нам нужен класс в котором у нас будет создаваться Notification по клику на который мы будем открывать видео.

NotificationWrapper.kt
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import dajver.com.recordscreenexample.R
import java.io.File

object NotificationWrapper {

    internal fun createNotification(context: Context, videoPath: File) {
        val channelId = "app_channel"

        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(videoPath!!.path))
        intent.setDataAndType(Uri.parse(videoPath.path), "video/mp4")

        val resultPendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(channelId, "AppExampleChannel", NotificationManager.IMPORTANCE_DEFAULT)
            channel.description = context.getString(R.string.app_name)
            channel.setSound(null, null)
            channel.enableLights(false)
            channel.lightColor = Color.BLUE
            channel.enableVibration(false)

            notificationManager.createNotificationChannel(channel)
        }

        val notification = NotificationCompat.Builder(context, channelId)
            .setContentTitle("Screen was recorded")
            .setContentText("You can open recorded video by clicking on this notification")
            .setContentIntent(resultPendingIntent)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setVibrate(null)
            .setDefaults(0)
            .setWhen(0)
            .setOnlyAlertOnce(true)
            .build()
        notificationManager.notify(777, notification)
    }
}
Просто создаем нотификейшн, в который передаем адрес файла для воспроизведения, и передаем его в интент который будет открывать видео. Так же мы задали канал для андроид 6 и выше что бы нотиф мог отображаться на них. Ну и далее уже типичное создаем NotificationCompat и задаем параметры отображения.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

    <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/startStopButton"
            android:text="Start recording"/>
</LinearLayout>
Просто кнопка, мы без излишеств. Скромные маленькие человечки.

MainActivity.kt
import android.Manifest.permission
import android.os.Bundle
import android.os.Environment
import androidx.appcompat.app.AppCompatActivity
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import dajver.com.recordscreenexample.recorder.RecorderManager
import dajver.com.recordscreenexample.recorder.service.listeners.RecordingPermissionListener
import dajver.com.recordscreenexample.recorder.service.listeners.StopRecordingListener
import dajver.com.recordscreenexample.wrappers.NotificationWrapper
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.util.*

class MainActivity : AppCompatActivity(), RecordingPermissionListener, StopRecordingListener,
    MultiplePermissionsListener {

    private var mRecorderManager: RecorderManager? = null

    private var isRecording: Boolean = false
    private var videoPath: File? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Dexter.withActivity(this).withPermissions(
                permission.WRITE_EXTERNAL_STORAGE,
                permission.READ_EXTERNAL_STORAGE,
                permission.RECORD_AUDIO
            ).withListener(this).check()

        mRecorderManager = RecorderManager(this, this, this)

        startStopButton.setOnClickListener {
            if(isRecording) {
                startStopButton.text = "Start recording"

                mRecorderManager!!.stopRecording(this)

                isRecording = false
            } else {
                startStopButton.text = "Stop recording"

                videoPath = getVideoFilePath()
                mRecorderManager!!.startRecording(videoPath!!)

                isRecording = true
            }
        }
    }

    private fun getVideoFilePath(): File {
        val videoId = UUID.randomUUID().toString()
        val rootSessionsDir = File(getExternalFilesDir(Environment.DIRECTORY_DCIM), ROOT_SESSIONS_NAME)
        if (!rootSessionsDir.exists() || !rootSessionsDir.isDirectory) {
            rootSessionsDir.mkdir()
        }
        val videoDir = File(rootSessionsDir, videoId)
        if (!videoDir.exists() || !videoDir.isDirectory) {
            videoDir.mkdir()
        }
        val gameName = String.format(Locale.US, GAME_FILE_NAME_FORMAT, (1..9999).random())
        val videoFileName = "$gameName.mp4"
        val videoFile = File(videoDir, videoFileName)
        if (!videoFile.exists() || !videoFile.isFile) {
            videoFile.createNewFile()
        }
        return videoFile
    }

    override fun onPermissionGranted() {
        // show some indicator that recording has started
    }

    override fun onPermissionDenied() {
        mRecorderManager!!.cancelRecording()
    }

    override fun onStopRecording() {
        // show some indicator that recording ended, for example show the notification about recorded video
        NotificationWrapper.createNotification(this, videoPath!!)
    }

    override fun onSystemStopRecording() {
        mRecorderManager!!.cancelRecording()
        // and remove all other objectives which you need
    }

    override fun onPermissionsChecked(report: MultiplePermissionsReport?) { }

    override fun onPermissionRationaleShouldBeShown(permissions: MutableList<PermissionRequest>?, token: PermissionToken?) { }

    companion object {
        private const val GAME_FILE_NAME_FORMAT = "video_%1\$d"
        private const val ROOT_SESSIONS_NAME = "videos"
    }
}
В onCreate мы спрашиваем у пользователя разрешения на запись экрана и на запись в внутренее хранилище. Так же вешаем лисенера на кнопку по нажатию на которую мы стартуем или стопаем запись экрана. Создаем инстанс RecorderManager и подписываемся на всех лисенеров на которые нас просит этот класс пописаться.
— getVideoFilePath — создает путь к файлу и сам файл в который мы будем писать.

Ну и дальше уже просто создаем кучу методов которые просит нас создать студия, это методы из наших лисеноров.

В onStopRecording мы показываем нотиф с помощью нашего класса NotificationWrapper по окончанию записи…

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools" package="dajver.com.recordscreenexample">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

    <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"
            tools:ignore="GoogleAppIndexingWarning">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <activity android:name=".recorder.activity.RequestMediaProjectionActivity" />

        <service android:name=".recorder.service.RecordService" />

    </application>

</manifest>
Так же еще осталось прописать все пермишенны в AndroidManifest, сервис и дополнительную активити которая будет вызываться для запроса на запись при клике на кнопку старта. Прописываем те же самые пермишены которые запрашивали в MainActivity.

Вот собственно и вся магия, надеюсь что хоть немного понятно объяснил весь этот треш который тут происходит. И это кому-то пригодится.

Исходники:
GitHub

Комментариев нет:

Отправить комментарий