Android应用在默认屏和虚拟屏之间进行移动

Android启动应用到虚拟屏和将应用在默认屏和虚拟屏之间进行移动。都在代码里了:

package com.tinnove.aiagent.base

import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.ActivityOptions
import android.content.Context
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.ImageReader
import android.os.Build
import android.util.Log
import android.view.Display
import java.lang.reflect.Method

/**
 * Description: AppDisplayMover
 * 该类用于:
 * 1)启动 App进程 到虚拟屏
 * 2)将 App进程 从默认屏 move 到虚拟屏
 * 3)将 App进程 从虚拟屏 move 到默认屏
 *
 * Author: platolei
 * Date: 2025/4/4 17:00
 */
object AppDisplayMover {

    private const val TAG = "AppDisplayMover"

    private const val DISPLAY_NAME = "AgentVirtualDisplay"

    @JvmField
    var virtualDisplayId = -1
    private var mDisplayManager: DisplayManager? = null
    private var mVirtualDisplay: VirtualDisplay? = null
    private var mImageReader: ImageReader? = null

    /**
     * 启动 App进程 到虚拟屏
     *
     * @param context
     * @param packageName
     */
    @JvmStatic
    fun launchAppByPackage(context: Context, packageName: String?) {
        val packageManager = context.packageManager
        val launchIntent = packageManager.getLaunchIntentForPackage(packageName!!)
        if (launchIntent != null) {
            if (virtualDisplayId == -1) {
                Log.e(TAG, "未初始化")
                throw RuntimeException("AppDisplayMover:未初始化")
            }
            val options = ActivityOptions.makeBasic()
            options.launchDisplayId = virtualDisplayId
            context.startActivity(launchIntent, options.toBundle())
        } else {
            Log.e(TAG, "未找到该应用,请检查包名")
        }
    }

    /**
     * 初始化虚拟屏
     *
     * @param context
     */
    @JvmStatic
    fun init(context: Context) {
        // 获取 DisplayManager 服务
        mDisplayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        val widthPixels = context.resources.displayMetrics.widthPixels
        val heightPixels = context.resources.displayMetrics.heightPixels
        // 创建 ImageReader 用于提供 Surface
        mImageReader = ImageReader.newInstance(
            widthPixels,
            heightPixels,
            PixelFormat.RGBA_8888,
            2
        )
        // 创建虚拟显示屏
        mVirtualDisplay = mDisplayManager!!.createVirtualDisplay(
            DISPLAY_NAME,
            widthPixels,
            heightPixels,
            context.resources.displayMetrics.densityDpi,
            mImageReader!!.surface,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY or
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
        )
        if (mVirtualDisplay != null) {
            // 获取虚拟屏幕的 Display ID
            virtualDisplayId = mVirtualDisplay!!.display.displayId
            Log.i(TAG, "虚拟屏幕创建成功,virtualDisplayId:$virtualDisplayId")
        }
    }

    /**
     * 将 App进程 从虚拟屏 move 到默认屏
     *
     * @param context     上下文
     * @param packageName 目标应用包名
     */
    @JvmStatic
    fun moveApp2DefaultDisplay(context: Context, packageName: String): Boolean {
        // 获取目标应用的任务 ID
        val taskId = getTaskIdByPackageName(context, packageName)
        if (taskId == -1) {
            Log.i(TAG, "未找到包名为 $packageName 的任务")
            return false
        }
        // 迁移任务到默认屏幕
        kotlin.runCatching {
            moveTaskToDisplay(taskId, getDefaultDisplayId(context))
            return true
        }.onFailure {
            Log.e(TAG, it.message.toString())
        }
        return false
    }

    /**
     * 将 App进程 从默认屏 move 到虚拟屏
     *
     * @param context     上下文
     * @param packageName 目标应用包名
     */
    @JvmStatic
    fun moveApp2VirtualDisplay(context: Context, packageName: String): Boolean {
        // 获取目标应用的任务 ID
        val taskId = getTaskIdByPackageName(context, packageName)
        if (taskId == -1) {
            Log.i(TAG, "未找到包名为 $packageName 的任务")
            return false
        }
        // 迁移任务到默认屏幕
        kotlin.runCatching {
            moveTaskToDisplay(taskId, virtualDisplayId)
            return true
        }.onFailure {
            Log.e(TAG, it.message.toString())
        }
        return false
    }

    /**
     * 获取默认屏幕的 Display ID
     *
     * @param context
     * @return
     */
    private fun getDefaultDisplayId(context: Context): Int {
        val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        val defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
        val defaultDisplayId = defaultDisplay?.displayId ?: -1
        Log.i(TAG, "defaultDisplayId:$defaultDisplayId")
        return defaultDisplayId
    }

    /**
     * 通过包名获取前台任务 ID
     *
     * @param context
     * @param packageName
     * @return
     */
    private fun getTaskIdByPackageName(context: Context, packageName: String): Int {
        val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        // Android 10+ 需使用新 API
        val tasks = am.getRunningTasks(100)
        for (task in tasks) {
            if (task.topActivity != null && task.topActivity!!.packageName == packageName) {
                return task.taskId
            }
        }
        return -1
    }

    /**
     * move 任务到指定屏幕
     *
     * @param taskId
     * @param displayId
     */
    private fun moveTaskToDisplay(taskId: Int, displayId: Int) {
        val activityTaskManagerService = getActivityTaskManagerService()
        activityTaskManagerService?.let {
            val clazz: Class<*> = activityTaskManagerService.javaClass
            val moveTaskToDisplayMethod: Method =
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                    clazz.getMethod(
                        "moveRootTaskToDisplay",
                        Int::class.javaPrimitiveType,
                        Int::class.javaPrimitiveType
                    )
                } else {
                    clazz.getMethod(
                        "moveStackToDisplay",
                        Int::class.javaPrimitiveType,
                        Int::class.javaPrimitiveType
                    )
                }
            moveTaskToDisplayMethod.isAccessible = true
            moveTaskToDisplayMethod.invoke(activityTaskManagerService, taskId, displayId)
        }
    }

    /**
     * 通过反射获取 ActivityTaskManagerService
     *
     * @return ActivityTaskManagerService
     */
    @SuppressLint("PrivateApi")
    private fun getActivityTaskManagerService(): Any? {
        val clazz = Class.forName("android.app.ActivityTaskManager")
        val getServiceMethod = clazz.getMethod("getService")
        getServiceMethod.isAccessible = true
        return getServiceMethod.invoke(null)
    }

    /**
     * 释放虚拟屏
     */
    @JvmStatic
    fun release() {
        if (mVirtualDisplay != null) {
            mVirtualDisplay!!.release()
            mVirtualDisplay = null
        }
        if (mImageReader != null) {
            mImageReader!!.close()
            mImageReader = null
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值