Как сохранить выданное разрешение MediaProjection?

Рейтинг: 0Ответов: 2Опубликовано: 12.02.2025

Я разрабатываю приложение на kotlin и у меня вопрос по android.media.projection.MediaProjection.

Как это должно работать:

  1. Установка приложения
  2. Выдача разрешений в том числе на запись экрана.
  3. С сервера инициализируется запрос на просмотр экрана с девайса

Обработка запроса на старт записи экрана от сервера:

private fun handleCommand(command: String) {
    when (command) {
        "START_SCREEN" -> {
            if (isStreaming) {
                Log.d(TAG, "Стриминг уже запущен")
                return
            }

            val projectionData = MediaProjectionStorage.loadProjectionData(this)
            Log.d(TAG, "Получена команда START_SCREEN, наличие данных: ${projectionData != null}")

            if (projectionData != null) {
                val (code, intent) = projectionData
                Log.d(TAG, "Загружены данные MediaProjection: code=$code, intent=${intent != null}")
                startStreaming()
            } else {
                Log.e(TAG, "Нет данных MediaProjection для запуска стрима")
            }
        }
        "STOP_SCREEN" -> {
            stopStreaming()
        }
    }
}

Запуск стрима:

private fun startStreaming() {
    if (!checkCodecSupport()) {
        Log.e(TAG, "Device doesn't support required video codec")
        return
    }

    try {
        cleanupMediaComponents()

        val projectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        val savedProjection = MediaProjectionStorage.loadProjectionData(this)

        if (savedProjection == null) {
            Log.e(TAG, "No saved MediaProjection data found")
            return
        }

        val (code, data) = savedProjection

        if (code != Activity.RESULT_OK) {
            Log.e(TAG, "Invalid projection result code: $code")
            MediaProjectionStorage.clearProjectionData(this)
            return
        }

        mediaProjection = projectionManager.getMediaProjection(code, data!!)

        if (mediaProjection == null) {
            Log.e(TAG, "Failed to create MediaProjection")
            MediaProjectionStorage.clearProjectionData(this)
            return
        }

        Log.d(TAG, "MediaProjection successfully created")

        // Rest of your existing setup code...
        setupMediaCodec()
        surface = mediaCodec?.createInputSurface()

        virtualDisplay = mediaProjection?.createVirtualDisplay(
            "ScreenCapture",
            screenWidth, screenHeight, screenDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            surface, null, null
        )

        mediaCodec?.start()
        isStreaming = true
        updateSocketTimeout()

        startScreenDataProcessing()
    } catch (e: Exception) {
        Log.e(TAG, "Error starting screen capture: ${e.message}")
        e.printStackTrace()
        stopStreaming()
    }
}

Остановка стрима:

private fun cleanupMediaComponents() {
    try {
        virtualDisplay?.release()
        mediaCodec?.stop()
        mediaCodec?.release()
        surface?.release()

        virtualDisplay = null
        mediaCodec = null
        surface = null
        mediaProjection = null
    } catch (e: Exception) {
        Log.e(TAG, "Error in cleanupMediaComponents: ${e.message}")
    }
}

private fun stopStreaming() {
    if (!isStreaming) return

    isStreaming = false
    try {
        cleanupMediaComponents()
        updateSocketTimeout()
    } catch (e: Exception) {
        Log.e(TAG, "Error stopping stream: ${e.message}")
    }
}

После остановки и следующем старте на девайсе вылазит опять запрос на выдачу разрешения.

Я хочу найти подход благодаря которому я смогу сохранить выданное 1 раз разрешение и использовать его на постоянной основе (до удаления приложения).

Вот та часть которая сохраняет вроде как, но на деле, я так понимаю, ничего подобного...

package com.example.audiostreamer.utils

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.util.Log

object MediaProjectionStorage{
    private const val TAG = "MediaProjectionPrefs"
    private const val PREFS_NAME = "media_projection_data"
    private const val KEY_PROJECTION_CODE = "projection_code"
    private const val KEY_INTENT_DATA = "intent_data"

    // Храним Intent в памяти
    private var cachedProjectionIntent: Intent? = null
    private var cachedResultCode: Int = Activity.RESULT_CANCELED

    fun saveProjectionData(context: Context, resultCode: Int, data: Intent) {
        try {
            // Кэшируем данные в памяти
            cachedProjectionIntent = data
            cachedResultCode = resultCode

            // Сохраняем в SharedPreferences
            val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            prefs.edit().apply {
                putInt(KEY_PROJECTION_CODE, resultCode)
                // Сохраняем Intent как Bundle
                data.extras?.let { bundle ->
                    putString(KEY_INTENT_DATA, android.util.Base64.encodeToString(
                        bundle.toString().toByteArray(),
                        android.util.Base64.DEFAULT
                    ))
                }
            }.apply()

            Log.d(TAG, "Successfully saved MediaProjection data")
        } catch (e: Exception) {
            Log.e(TAG, "Failed to save MediaProjection data", e)
        }
    }

    fun loadProjectionData(context: Context): Pair<Int, Intent?>? {
        try {
            // Сначала пробуем использовать кэшированные данные
            if (cachedProjectionIntent != null && cachedResultCode == Activity.RESULT_OK) {
                return Pair(cachedResultCode, cachedProjectionIntent)
            }

            val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            val resultCode = prefs.getInt(KEY_PROJECTION_CODE, Activity.RESULT_CANCELED)
            val intentData = prefs.getString(KEY_INTENT_DATA, null)

            if (resultCode != Activity.RESULT_OK || intentData == null) {
                return null
            }

            // Восстанавливаем Intent
            try {
                val decodedBundle = android.util.Base64.decode(intentData, android.util.Base64.DEFAULT)
                val bundle = android.os.Bundle()
                bundle.putString("data", String(decodedBundle))

                val intent = Intent()
                intent.putExtras(bundle)

                // Кэшируем восстановленные данные
                cachedProjectionIntent = intent
                cachedResultCode = resultCode

                return Pair(resultCode, intent)
            } catch (e: Exception) {
                Log.e(TAG, "Failed to restore intent data", e)
                return null
            }
        } catch (e: Exception) {
            Log.e(TAG, "Failed to load MediaProjection data", e)
            return null
        }
    }

    fun clearProjectionData(context: Context) {
        cachedProjectionIntent = null
        cachedResultCode = Activity.RESULT_CANCELED

        context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
            .edit()
            .clear()
            .apply()
    }
}

Таким образом при получении разрешение после установки я пробую сохранить это разрешение в MainActivity в методе onActivityResult

MEDIA_PROJECTION_REQUEST -> {
    if (resultCode == RESULT_OK && data != null) {
        try {
            Log.d("MainActivity", "Получены данные MediaProjection с resultCode=RESULT_OK")

            // Important change: Don't create a new Intent, use the original one
            MediaProjectionStorage.saveProjectionData(this, resultCode, data)

            // Проверяем сохранение
            if (MediaProjectionStorage.loadProjectionData(this)?.first == RESULT_OK) {
                Log.d("MainActivity", "MediaProjection данные сохранены успешно")

                // Устанавливаем те же данные в сервис
                ScreenStreamingService.setMediaProjectionData(this, resultCode, data)

                // Запускаем сервисы
                startRequiredServices()
            } else {
                Log.e("MainActivity", "Ошибка сохранения MediaProjection данных")
                MediaProjectionStorage.clearProjectionData(this)
                requestScreenCapture()
            }
        } catch (e: Exception) {
            Log.e("MainActivity", "Ошибка обработки MediaProjection", e)
            MediaProjectionStorage.clearProjectionData(this)
            requestScreenCapture()
        }
    } else {
        Log.e("MainActivity", "MediaProjection не получен: resultCode=$resultCode")
        requestScreenCapture()
    }
}

По итогу, разрешение всё равно запрашивается повторно.

Можно ли сохранить выданное разрешение MediaProjection и запрашивать его повторно для дальнейших стримов экрана без подтверждения от пользователя?

И если нет такой возможности, то реально ли использовать захват экрана без MediaProjection, например через AccessibilityService?

Ответы

▲ 1

Документация ясно говорит, что разрешения пользователя должны запрашиваться каждый раз.

Your app must request user consent before each media projection session.

https://developer.android.com/media/grow/media-projection#user_consent

▲ -1

Я решил вопрос, путём реализации сохранения состояния, теперь при каждой инициализации с сервера на телефоне юзера нет подтверждения :) победа короче