需要先通过getTasks获取系统任务列表,系统应用才有权限,否则需要使用 ActivityManager.getAppTasks()
val recentTasks = ActivityTaskManager.getInstance().getRecentTasks(Int.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, Process.myUserHandle().getIdentifier())
接着从TaskInfo中读取应用信息,appName,appIcon,最近快照,需要从task的baseIntent中构建intent对象
val intent = Intent(task.baseIntent)
intent.component = task.baseActivity
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") }
如果resolveActivity方法不为空,可以直接获取 resolveInfo.activityInfo,然后拿到应用标题跟图标
val title = activityInfo.loadLabel(packageManager).toString()
val icon = activityInfo.loadIcon(packageManager).toBitmap()
任务快照则需要通过 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) } } } }
缩放原图,减少加载内存跟时长
如果需要回到首页
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") }
加载任务列表时,先加载任务,然后异步加载图片,避免最近任务延迟加载
private fun loadData() { Log.d(TAG, "loadData") mJob?.cancel() mJob = lifecycleScope.launch { taskAdapter.setData(getRecentTasks()) withContext(Dispatchers.IO) { setTaskThumbnail() } taskAdapter.notifyDataSetChanged() } }
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 }
打开任务应用
而进入任务应用,需要使用 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)
该方法是从最近任务恢复复用现有实例,会触发 onNewIntent()
跟startActivity的不同是直接start可能导致重复创建实例,并且二者系统处理逻辑也不一样,系统默认行为存在差异
多屏处理
if (displayId != null && task.displayId != displayId) continue
如果有多屏需要考虑displayId,因为最近任务会加载所有屏幕打开的最近任务,而在当前屏幕下只需要加载当前屏幕下的最近任务
白名单处理
val hasTask = filterTask.contains(pkg) if (hasTask) continue
如果需要屏蔽某个应用的任务,需要维护白名单,在最近任务列表中单独处理
结束任务
使用 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) } }
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 }
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) }
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>
如果是全屏界面隐藏状态栏
window.isNavigationBarContrastEnforced = false WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.getInsetsController(window, window.decorView).apply { systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE hide(WindowInsetsCompat.Type.systemBars()) }
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() } }
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) } } } }
<?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" />
浙公网安备 33010602011771号