chromium/chrome/browser/util/android/java/src/org/chromium/chrome/browser/util/AndroidTaskUtils.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.util;

import android.app.ActivityManager;
import android.app.ActivityManager.AppTask;
import android.app.ActivityManager.RecentTaskInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.Nullable;

import org.chromium.base.ContextUtils;
import org.chromium.base.PackageManagerUtils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/** Deals with Document-related API calls. */
public class AndroidTaskUtils {
    public static final String TAG = "DocumentUtilities";

    // Typically the number of tasks returned by getRecentTasks will be around 3 or less - the
    // Chrome Launcher Activity, a Tabbed Activity task, and the home screen on older Android
    // versions. However, theoretically this task list could be unbounded, so limit it to a number
    // that won't cause Chrome to blow up in degenerate cases.
    private static final int MAX_NUM_TASKS = 100;

    /**
     * Finishes tasks other than the one with the given ID that were started with the given data
     * in the Intent, removing those tasks from Recents and leaving a unique task with the data.
     * @param data Passed in as part of the Intent's data when starting the Activity.
     * @param canonicalTaskId ID of the task will be the only one left with the ID.
     * @return Intent of one of the tasks that were finished.
     */
    public static Intent finishOtherTasksWithData(Uri data, int canonicalTaskId) {
        if (data == null) return null;

        String dataString = data.toString();
        Context context = ContextUtils.getApplicationContext();

        ActivityManager manager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.AppTask> tasksToFinish = new ArrayList<ActivityManager.AppTask>();
        for (ActivityManager.AppTask task : manager.getAppTasks()) {
            RecentTaskInfo taskInfo = getTaskInfoFromTask(task);
            if (taskInfo == null) continue;
            int taskId = taskInfo.id;

            Intent baseIntent = taskInfo.baseIntent;
            String taskData = baseIntent == null ? null : taskInfo.baseIntent.getDataString();

            if (TextUtils.equals(dataString, taskData)
                    && (taskId == -1 || taskId != canonicalTaskId)) {
                tasksToFinish.add(task);
            }
        }
        return finishAndRemoveTasks(tasksToFinish);
    }

    private static Intent finishAndRemoveTasks(List<ActivityManager.AppTask> tasksToFinish) {
        Intent removedIntent = null;
        for (ActivityManager.AppTask task : tasksToFinish) {
            Log.d(TAG, "Removing task with duplicated data: " + task);
            removedIntent = getBaseIntentFromTask(task);
            task.finishAndRemoveTask();
        }
        return removedIntent;
    }

    /**
     * Returns the RecentTaskInfo for the task, if the ActivityManager succeeds in finding the task.
     * @param task AppTask containing information about a task.
     * @return The RecentTaskInfo associated with the task, or null if it couldn't be found.
     */
    public static RecentTaskInfo getTaskInfoFromTask(AppTask task) {
        RecentTaskInfo info = null;
        try {
            info = task.getTaskInfo();
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Failed to retrieve task info: ", e);
        }
        return info;
    }

    /**
     * Returns the baseIntent of the RecentTaskInfo associated with the given task.
     * @param task Task to get the baseIntent for.
     * @return The baseIntent, or null if it couldn't be retrieved.
     */
    public static Intent getBaseIntentFromTask(AppTask task) {
        RecentTaskInfo info = getTaskInfoFromTask(task);
        return info == null ? null : info.baseIntent;
    }

    /**
     * Given an AppTask retrieves the task component name.
     * @param task The app task to use.
     * @return Fully qualified component name name or null if we were not able to
     * determine it.
     */
    public static String getTaskComponentName(AppTask task) {
        RecentTaskInfo info = getTaskInfoFromTask(task);
        if (info == null) return null;

        Intent baseIntent = info.baseIntent;
        if (baseIntent == null) {
            return null;
        } else if (baseIntent.getComponent() != null) {
            return baseIntent.getComponent().getClassName();
        } else {
            ResolveInfo resolveInfo = PackageManagerUtils.resolveActivity(baseIntent, 0);
            if (resolveInfo == null) return null;
            return resolveInfo.activityInfo.name;
        }
    }

    /**
     * Get all recent tasks with component name matching any of the given names.
     * @param context the Android Context
     * @param componentsAccepted the set of names accepted
     * @return all matching recent {@link AppTask} and their respective {@link RecentTaskInfo}
     */
    public static Set<Pair<AppTask, RecentTaskInfo>> getRecentAppTasksMatchingComponentNames(
            Context context, Set<String> componentsAccepted) {
        HashSet<Pair<AppTask, RecentTaskInfo>> matchingTasks = new HashSet<>();

        ActivityManager manager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

        for (AppTask task : manager.getAppTasks()) {
            RecentTaskInfo info = AndroidTaskUtils.getTaskInfoFromTask(task);
            if (info == null) continue;
            String componentName = AndroidTaskUtils.getTaskComponentName(task);

            if (componentsAccepted.contains(componentName)) {
                matchingTasks.add(Pair.create(task, info));
            }
        }
        return matchingTasks;
    }

    /**
     * Get all recent tasks infos with component name matching any of the given names.
     * @param context the Android Context
     * @param componentsAccepted the set of names accepted
     * @return all matching {@link RecentTaskInfo}s
     */
    public static Set<RecentTaskInfo> getRecentTaskInfosMatchingComponentNames(
            Context context, Set<String> componentsAccepted) throws SecurityException {
        HashSet<RecentTaskInfo> matchingInfos = new HashSet<>();

        final ActivityManager activityManager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

        // getRecentTasks is deprecated, but still returns your app's tasks, and does so
        // without needing an extra IPC for each task you want to get the info for. It also
        // includes some known-safe tasks like the home screen on older Android versions, but
        // that's fine for this purpose.
        List<ActivityManager.RecentTaskInfo> tasks =
                activityManager.getRecentTasks(MAX_NUM_TASKS, 0);
        if (tasks != null) {
            for (ActivityManager.RecentTaskInfo task : tasks) {
                // Note that Android documentation lies, and TaskInfo#origActivity does not
                // actually return the target of an alias, so we have to explicitly check
                // for the target component of the base intent, which will have been set to
                // the Activity that launched, in order to make this check more robust.
                ComponentName component = task.baseIntent.getComponent();
                if (component == null) continue;
                if (componentsAccepted.contains(component.getClassName())
                        && component.getPackageName().equals(context.getPackageName())) {
                    matchingInfos.add(task);
                }
            }
        }
        return matchingInfos;
    }

    /**
     * Get the {@link AppTask} for a given taskId.
     *
     * @param context The activity context.
     * @param taskId The id of the task whose AppTask will be returned.
     * @return The {@link AppTask} for a given taskId if found, {@code null} otherwise.
     */
    public static @Nullable AppTask getAppTaskFromId(Context context, int taskId) {
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        for (var appTask : am.getAppTasks()) {
            var taskInfo = appTask.getTaskInfo();
            if (taskInfo == null) continue;
            int taskInfoId = taskInfo.id;
            if (VERSION.SDK_INT >= VERSION_CODES.Q) {
                taskInfoId = taskInfo.taskId;
            }
            if (taskInfoId == taskId) {
                return appTask;
            }
        }
        return null;
    }
}