需要先通过getTasks获取系统任务列表,系统应用才有权限,否则需要使用 ActivityManager.getAppTasks()

val recentTasks = ActivityTaskManager.getInstance().getRecentTasks(Int.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, Process.myUserHandle().getIdentifier())
View Code

接着从TaskInfo中读取应用信息,appName,appIcon,最近快照,需要从task的baseIntent中构建intent对象

val intent = Intent(task.baseIntent)
intent.component = task.baseActivity
View Code

TaskInfo中有四个属性字段

topActivity:当前任务栈顶

origActivity:最初启动的任务栈

realActivity:实际处理的任务栈

baseActivity:根任务栈

获取对应的应用名称跟图标,需要借助 ActivityInfo,不过在此之前需要需要先判断 resolveActivity()

若系统中没有能处理该 Intent 的 Activity(如应用已被卸载或 Intent 配置错误),会返回 null。此时直接使用会导致 NullPointerException

在读取最近任务时,其他应用的 Activity 可能因权限限制、组件禁用或版本差异无法解析,提前判空可避免崩溃

val resolveInfo = packageManager.resolveActivity(intent, 0)
if (resolveInfo == null) {
    log("getRecentTasks resolveInfo is null $resolveInfo")
}
View Code

如果resolveActivity方法不为空,可以直接获取 resolveInfo.activityInfo,然后拿到应用标题跟图标

val title = activityInfo.loadLabel(packageManager).toString()
val icon = activityInfo.loadIcon(packageManager).toBitmap()
View Code

任务快照则需要通过 ActivityManagerWrapper.getInstance().getTaskThumbnail(task.taskId, false)?.thumbnail 获取,耗时操作,需要放子线程

private suspend fun setTaskThumbnail() {
        Log.d(TAG, "setTaskThumbnail")
        for (app in taskAdapter.getData()) {
            currentCoroutineContext().ensureActive()
            ActivityManagerWrapper.getInstance()
                .getTaskThumbnail(app.taskId, false)?.thumbnail?.let {
                    val w = it.width
                    val h = it.height
                    Log.d(TAG, "${app.title} pkg ${app.packageName} thumbnail width $w height $h")
                    if (w > 0 && h > 0) {
                        app.thumbnailBmp = it.scale(w / 3, h / 3, false)
                    }
                }
        }
    }
View Code

缩放原图,减少加载内存跟时长

如果需要回到首页

private fun returnToHome() {
        Log.d(TAG, "returnToHome")
        val intent = Intent(Intent.ACTION_MAIN)
            .addCategory(Intent.CATEGORY_HOME)
            .setPackage(requireContext().packageName)
        requireContext().startActivity(intent)
        finish("returnToHome")
    }
View Code

加载任务列表时,先加载任务,然后异步加载图片,避免最近任务延迟加载

    private fun loadData() {
        Log.d(TAG, "loadData")
        mJob?.cancel()
        mJob = lifecycleScope.launch {
            taskAdapter.setData(getRecentTasks())
            withContext(Dispatchers.IO) {
                setTaskThumbnail()
            }
            taskAdapter.notifyDataSetChanged()
        }
    }
View Code
private fun getRecentTasks(context: Context, maxLength: Int = 30): Collection<MyAppInfo> {
        list.clear()
        val recentTasks = ActivityTaskManager.getInstance()
            .getRecentTasks(
                Int.MAX_VALUE,
                ActivityManager.RECENT_IGNORE_UNAVAILABLE,
                Process.myUserHandle().getIdentifier()
            )
        log("getRecentTasks size ${recentTasks.size}")
        val packageManager = context.packageManager
        if (packageManager == null) {
            log("getRecentTasks packageManager is null")
            return list
        }
        for (task in recentTasks) {
            if (list.size >= maxLength) {
                log("getRecentTasks break")
                break
            }
            val intent = Intent(task.baseIntent)
            if (task.baseActivity != null) {
                intent.component = task.baseActivity
            }
            // 设置intent的启动方式为 创建新task()【并不一定会创建】
            intent.flags = (intent.flags and Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED.inv()
                    or Intent.FLAG_ACTIVITY_NEW_TASK)
            val resolveInfo = packageManager.resolveActivity(intent, 0)
            if (resolveInfo == null) {
                log("getRecentTasks resolveInfo is null $resolveInfo")
                return list
            }
            val myAppInfo = MyAppInfo()
            myAppInfo.taskId = task.taskId
            try {
                val activityInfo = resolveInfo.activityInfo
                val pkg = activityInfo.packageName
                val title = activityInfo.loadLabel(packageManager).toString()
                myAppInfo.packageName = pkg
                myAppInfo.displayId = task.displayId
                log("getRecentTasks resolveInfo=$resolveInfo,title=$title,taskId=${task.taskId},displayId=${task.displayId}")
                val hasTask = filterTask.contains(pkg)
                if (hasTask) continue
                if (displayId != null && task.displayId != displayId) continue
                myAppInfo.title = title
                myAppInfo.appName = title
                myAppInfo.appLogo = activityInfo.loadIcon(packageManager).toBitmap()
            } catch (e: Exception) {
                Log.e(TAG, "getRecentTasks ", e)
            }
            list.add(myAppInfo)
            log("getRecentTasks add task ${myAppInfo.title},pkg=${myAppInfo.packageName}")
        }
        return list
    }
View Code

打开任务应用

而进入任务应用,需要使用 ActivityManagerWrapper.getInstance().startActivityFromRecents()

val options = ActivityOptions.makeBasic()
options.launchDisplayId = displayId
val bundle = options.toBundle()
bundle.putInt("android.activity.windowingMode", 0)
ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId, options)
View Code

该方法是从最近任务恢复复用现有实例,会触发 onNewIntent()

跟startActivity的不同是直接start可能导致重复创建实例,并且二者系统处理逻辑也不一样,系统默认行为存在差异

多屏处理
if (displayId != null && task.displayId != displayId) continue
View Code

如果有多屏需要考虑displayId,因为最近任务会加载所有屏幕打开的最近任务,而在当前屏幕下只需要加载当前屏幕下的最近任务

白名单处理
val hasTask = filterTask.contains(pkg)
if (hasTask) continue
View Code

如果需要屏蔽某个应用的任务,需要维护白名单,在最近任务列表中单独处理

结束任务

使用 ActivityManager.forceStopPackage(pkgName) 强制停止应用,包括后台服务、定时任务等

但是有些场景可能需要应用自己处理结束,需要保留服务,这种情况下需要特殊处理

private fun stopProcessIfExist(am: ActivityManager, pkgName: String, taskId: Int? = null) {
        log("stopProcessIfExist taskId:$taskId,pkg:$pkgName")
        taskId?.let {
            ActivityManagerWrapper.getInstance().removeTask(it)
        }
        if (Constant.PACKAGE_TEST == pkgName) {
            am.runningAppProcesses?.let { processList ->
                log("stopProcessIfExist processList size ${processList.size}")
                for (i in processList.indices) {
                    val info = processList[i]
                    if (info != null && pkgName == info.processName) {
                        Log.w(TAG, "processName=${info.processName} pid=${info.pid}")
                        if (Constant.PACKAGE_TEST == info.processName) {
                            Process.killProcess(info.pid)
                        }
                    }
                }
            }
        } else if (Constant.PACKAGE_TEST_SERVICE == pkgName) {
            //do nothing
        } else {
            log("force-stop pkg:$pkgName")
            am.forceStopPackage(pkgName)
        }
    }
View Code

Process.killProcess(info.pid) 仅终止目标进程,所以需要配合ActivityManagerWrapper.getInstance().removeTask(it)一起使用

而结束任务杀进程同样是耗时操作,根据情况需要放到子线程处理

内存信息

    private fun getMemoryInfo(): ActivityManager.MemoryInfo? {
        try {
            val am =
                Utils.getApp().getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
            val mi = ActivityManager.MemoryInfo()
            am?.getMemoryInfo(mi)
            return mi
        } catch (e: Exception) {
            Log.e(TAG, "loadMemory :$e")
        }
        return null
    }
View Code
    fun loadMemory(): Pair<String, Double> {
        val info = getMemoryInfo()
        val totalMem = info?.totalMem ?: 0
        val availMem = info?.availMem ?: 0
        log("loadMemory totalMem $totalMem avail $availMem")
        val total: Double = if (totalMem == 0L) 16.0 else {
            totalMem / (1024 * 1024 * 1024.0)
        }
        val avail: Double = if (availMem == 0L) 0.0 else availMem / (1024 * 1024 * 1024.0)
        log("loadMemory total $total avail $avail")
        val using = Utils.getApp().resources.getString(R.string.memory_using)
        val memoryText = "$using ${df.format(total - avail)} / ${df.format(total)}GB"
        val progressNum = (total - avail) / total
        val currentProgress = progressNum * 100
        log("loadMemory progressNum $progressNum currentProgress $currentProgress")
        return Pair(memoryText, currentProgress)
    }
View Code
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">

    <uses-permission android:name="android.permission.REORDER_TASKS" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-sdk   tools:overrideLibrary="com.android.systemui.shared"/>
    <application>
        <activity
            android:name=".RecentTaskActivity"
            android:launchMode="singleTask"
            android:theme="@style/Theme.RecentTask"
            android:taskAffinity=".recents"
            android:process=".recents"
            android:configChanges="mcc|mnc|locale|layoutDirection|uiMode|touchscreen|keyboard|keyboardHidden|screenLayout|smallestScreenSize|fontScale|navigation"
            android:exported="true" >
            <intent-filter>
                <action android:name="com.zeekr.platform.service.intent.action.RECENT_TASK_ACTIVITY"/>
            </intent-filter>
            <meta-data
                android:name="distractionOptimized"
                android:value="true" />
        </activity>
    </application>

</manifest>
View Code

如果是全屏界面隐藏状态栏

window.isNavigationBarContrastEnforced = false
        WindowCompat.setDecorFitsSystemWindows(window, false)
        WindowCompat.getInsetsController(window, window.decorView).apply {
            systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
            hide(WindowInsetsCompat.Type.systemBars())
        }
View Code
package com.test.recent_task.utils

import android.animation.ValueAnimator
import android.util.Log
import android.view.View
import android.view.animation.AnimationUtils
import android.view.animation.DecelerateInterpolator
import androidx.core.animation.doOnEnd
import androidx.recyclerview.widget.RecyclerView
import com.test.recent_task.R
import com.test.recent_task.interpolator.CubicBezierInterpolator

class TaskAnimHelper(
    private val root: View,
    private val bgView: View,
    private val recyclerView: RecyclerView,
    private val linInfo: View,
    private var exitListener: ((isCloseAll: Boolean) -> Unit)
) {

    companion object {
        private const val TAG = "TaskAnimHelper"
    }

    private var rootHeight: Int = 0

    private var isExit = false
    private var isCloseAll = false

    private val animList = hashSetOf<ValueAnimator>()

    private val bgAlphaAnim = ValueAnimator.ofFloat(1f, 0f).apply {
        duration = 300
        startDelay = 200
        interpolator = CubicBezierInterpolator.BG_ALPHA
        addUpdateListener { animator ->
            val num = animator.animatedValue as Float // val scale = 1 + num * 0.3f
            (if (isCloseAll) root else bgView).alpha = num
        }
        animList.add(this)
        doOnEnd {
            exitListener.invoke(isCloseAll)
        }
    }

    private val contentScaleAnim = ValueAnimator.ofFloat(1f, 1.3f).apply {
        duration = 500
        interpolator = CubicBezierInterpolator.CONTENT_SCALE
        addUpdateListener { animator ->
            val num = animator.animatedValue as Float
            root.scaleX = num
            root.scaleY = num
        }
        animList.add(this)
    }

    private val contentAlphaAnim = ValueAnimator.ofFloat(1f, 0f).apply {
        duration = 350
        interpolator = CubicBezierInterpolator.CONTENT_ALPHA
        addUpdateListener { animator ->
            val num = animator.animatedValue as Float
            recyclerView.alpha = num
            linInfo.alpha = num
        }
        animList.add(this)
    }

    private val scaleAnim = ValueAnimator.ofFloat(1.3f, 1f).apply {
        duration = 600
        interpolator = DecelerateInterpolator(3f)
        addUpdateListener { animator ->
            val num = animator.animatedValue as Float
            root.scaleX = num
            root.scaleY = num
        }
        animList.add(this)
    }

    private val alphaAnim = ValueAnimator.ofFloat(0f, 1f).apply {
        duration = 600
        interpolator = DecelerateInterpolator(3f)
        addUpdateListener { animator ->
            val num = animator.animatedValue as Float
            bgView.alpha = num
        }
        animList.add(this)
    }

    private val closeAnim = ValueAnimator.ofFloat(0f, 1f).apply {
        duration = 500
        interpolator = DecelerateInterpolator(3f)
        addUpdateListener { animator ->
            val num = animator.animatedValue as Float
            linInfo.translationY = (rootHeight - linInfo.top) * num
            val scale = 1 + num / 3
            linInfo.scaleX = scale
            linInfo.scaleY = scale
        }
        animList.add(this)
    }

    fun startLoadAnim() {
        Log.i(TAG, "startLoadAnim")
        isExit = false
        scaleAnim.start()
        alphaAnim.start()
    }

    fun setLoadLayoutAnim() {
        Log.i(TAG, "setLoadLayoutAnim")
        recyclerView.layoutAnimation?.animation?.cancel()
        recyclerView.layoutAnimation =
            AnimationUtils.loadLayoutAnimation(recyclerView.context, R.anim.anim_task_list_load)
    }

    fun exitTaskAnim() {
        Log.i(TAG, "exitTaskAnim")
        if (isExit) return
        isCloseAll = false
        isExit = true
        bgAlphaAnim.start()
        contentScaleAnim.start()
        contentAlphaAnim.start()
    }

    fun closeAnim() {
        if (isExit) return
        isCloseAll = true
        isExit = true
        rootHeight = (recyclerView.parent as? View)?.height ?: 0
        Log.i(TAG, "closeAnim rootHeight $rootHeight")
        recyclerView.layoutAnimation?.animation?.cancel()
        recyclerView.layoutAnimation =
            AnimationUtils.loadLayoutAnimation(recyclerView.context, R.anim.anim_task_list_exit)
        recyclerView.startLayoutAnimation()
        closeAnim.start()
        bgAlphaAnim.start()
    }

    fun clear() {
        Log.i(TAG, "clear")
        recyclerView.layoutAnimation?.animation?.cancel()
        animList.forEach {
            it.cancel()
            it.removeAllListeners()
            it.removeAllUpdateListeners()
        }
        animList.clear()
    }

}
TaskAnimHelper
package com.test.recent_task

import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.core.graphics.scale
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.blankj.utilcode.util.ClickUtils
import com.test.recent_task.adapter.RecentTaskAdapter
import com.test.recent_task.bean.TaskBean
import com.test.recent_task.databinding.ActivityRecentTaskBinding
import com.test.recent_task.ext.ViewExt.setExtBackgroundColor
import com.test.recent_task.ext.ViewExt.setExtTextColor
import com.test.recent_task.ext.ViewExt.setPXTextSize
import com.test.recent_task.model.RecentTaskViewModel
import com.test.recent_task.utils.RecentTaskHelper
import com.test.recent_task.utils.TaskAnimHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
 * @author liuzhen
 * 任务管理器弹框
 */
class RecentTaskDialog : DialogFragment() {

    private lateinit var mBinding: ActivityRecentTaskBinding
    private val mViewModel by activityViewModels<RecentTaskViewModel>()

    private val taskAdapter = RecentTaskAdapter()

    private var clickTask: TaskBean? = null
    private val animHelper by lazy {
        TaskAnimHelper(mBinding.root, mBinding.viewBg, mBinding.taskRv, mBinding.linInfo) {
            if (it) returnToHome() else dismiss(clickTask)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        Log.i(TAG, "onCreateView")
        mBinding = ActivityRecentTaskBinding.inflate(inflater, container, false)
        return mBinding.getRoot()
    }

    override fun onCreateDialog(savedInstanceState: Bundle?) = Dialog(
        requireContext(), R.style.Theme_RecentTask
    ).apply {
        Log.i(TAG, "onCreateDialog")
        window?.apply {
            setLayout(
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.MATCH_PARENT
            )
            setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
        }
        setOnKeyListener { _, keyCode, event ->
            Log.i(TAG, "back keyCode $keyCode action ${event.action}")
            if (keyCode == KeyEvent.KEYCODE_BACK && event.action == 0) {
                exitTask("keyBack")
            }
            false
        }
    }

    private fun initTaskList() {
        mBinding.taskRv.apply {
            adapter = taskAdapter
            removeTaskListener = { removeTask(it) }
            taskClickListener = { clickTask(it) }
            taskAdapter.taskClickListener = { clickTask(it) }
            rootClickListener = { exitTask("listRootClick") }
        }
    }

    private fun removeTask(position: Int) {
        taskAdapter.removeTask(position)?.let {
            RecentTaskHelper.removeTask(requireContext(), it)
            if (taskAdapter.itemCount <= 0) {
                exitTask("removeTask")
            }
            loadMemory()
        }
    }

    override fun onStart() {
        super.onStart()
        Log.i(TAG, "onStart")
        mBinding.apply {
            initTaskList()
            root.setOnClick { exitTask("rootClick") }
            editButton.setOnClickListener { exitTask("editButtonClick") }
            ClickUtils.applySingleDebouncing(closeAll) {
                Log.i(TAG, "onClick close all")
                RecentTaskHelper.removeAllTask(taskAdapter.getData())
                animHelper.closeAnim()
            }
        }
        animHelper.startLoadAnim()
        loadData()
    }

    override fun onResume() {
        super.onResume()
        Log.i(TAG, "onResume")
    }

    override fun show(manager: FragmentManager, tag: String?) {
        super.show(manager, tag)
        Log.i(TAG, "show $tag")
    }

    private fun exitTask(tag: String) {
        Log.i(TAG, "tag $tag exitTask")
        animHelper.exitTaskAnim()
    }

    private fun dismiss(appInfo: TaskBean?) {
        Log.i(TAG, "dismiss")
        mViewModel.exit(appInfo)
        dismissAllowingStateLoss()
    }

    override fun dismiss() {
        dismiss(null)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.i(TAG, "onDestroy")
        isShow = false
        taskAdapter.clear()
        animHelper.clear()
        clickTask = null
    }

    @SuppressLint("NotifyDataSetChanged")
    private fun loadData() {
        Log.i(TAG, "loadData")
        loadMemory()
        lifecycleScope.launch {
            val list = getRecentTasks()
            taskAdapter.setData(list)
            if (list.size > 1) {
                animHelper.setLoadLayoutAnim()
            }
            withContext(Dispatchers.IO) { setTaskThumbnail() }
            delay(200)// 必要延迟,否则layoutAnimation冲突导致后面几个item失效
            taskAdapter.notifyDataSetChanged()
            Log.i(TAG, "loadData end")
        }
    }

    private fun loadMemory() {
        val info = mViewModel.loadMemory()
        mBinding.tvMemory.text = info.first
        mBinding.progress.progress = info.second.toInt()
    }

    private fun getRecentTasks() = RecentTaskHelper.getTaskList(requireContext())

    private suspend fun setTaskThumbnail() {
        Log.i(TAG, "setTaskThumbnail")
        for (i in 0 until taskAdapter.itemCount) {
            currentCoroutineContext().ensureActive()
            val app = taskAdapter.getItem(i)
            if (app == null) continue
            mViewModel.getTaskThumbnail(app.taskId)?.let {
                val w = it.width
                val h = it.height
                Log.i(TAG, "${app.title} pkg ${app.packageName} thumbnail width $w height $h")
                if (w > 0 && h > 0) {
                    app.thumbnailBmp = it.scale(w / 3, h / 3, false)
                }
            }
        }
        Log.i(TAG, "setTaskThumbnail end")
    }

    private fun clickTask(position: Int) {
        Log.i(TAG, "clickTask position $position")
        taskAdapter.getItem(position)?.let {
            clickTask = it
            animHelper.exitTaskAnim()
        }
    }

    private fun returnToHome() {
        Log.i(TAG, "returnToHome")
        val intent = Intent(Intent.ACTION_MAIN)
            .addCategory(Intent.CATEGORY_HOME)
            .setPackage(requireContext().packageName)
        requireContext().startActivity(intent)
        dismiss()
    }

    @SuppressLint("NotifyDataSetChanged")
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        Log.i(TAG, "onConfigurationChanged uiMode ${newConfig.uiMode} locales ${newConfig.locales}")
        taskAdapter.notifyDataSetChanged()
        mBinding.tvMemory.setExtTextColor(R.color.tv_memory_color)
        mBinding.tvMemory.setPXTextSize(R.dimen.close_all_text_size)
        mBinding.closeAll.setPXTextSize(R.dimen.close_all_text_size)
        mBinding.closeAll.dayNightChange()
        mBinding.editButton.dayNightChange()
        mBinding.deleteText.setExtTextColor(R.color.app_name_color)
        mBinding.deleteText.setPXTextSize(R.dimen.edit_text_size)
        mBinding.viewBg.setExtBackgroundColor(R.color.recent_task_background_color)
    }

    companion object {
        const val TAG = "RecentTaskDialog"
        var isShow = false
        fun show(manager: FragmentManager) {
            Log.i(TAG, "RecentTaskDialog isShow $isShow")
            if (!isShow) {
                isShow = true
                RecentTaskDialog().show(manager, TAG)
            }
        }
    }

}
RecentTaskDialog
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/anim_task_item_load"
    android:animationOrder="normal"
    android:delay="16.7%" />

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@anim/task_decelerate_interpolator"
    android:shareInterpolator="true">
    <alpha
        android:duration="600"
        android:fromAlpha="0.0"
        android:startOffset="100"
        android:toAlpha="1.0" />
    <scale
        android:duration="600"
        android:fromXScale="1.3"
        android:fromYScale="1.3"
        android:pivotX="50%"
        android:pivotY="50%"
        android:startOffset="0"
        android:toXScale="1.0"
        android:toYScale="1.0" />

</set>

<?xml version="1.0" encoding="utf-8"?>
<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
    android:factor="3" />
layoutAnimation

 

posted on 2025-10-15 10:37  翻滚的咸鱼  阅读(89)  评论(0)    收藏  举报