package com.example.android.intentplayground; import static java.util.stream.Collectors.toList; import android.app.Activity; import android.util.Log; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; /** * Provides information about the current runnings tasks and activities in the system, by tracking * all the lifecycle events happening in the app using {@link Tracker}. {@link Tracker} can be * observed for changes in this state. Information regarding the order of activities is kept inside * {@link Task}. */ public class Tracking { /** * Stores the {@link com.android.server.wm.Task}-s in MRU order together with the activities * within that task and their order. Classes can be notified of changes in this state through * {@link Tracker#addListener(Consumer)} */ public static class Tracker { private static final String TAG = "Tracker"; /** * Stores {@link Task} by their id. */ private HashMap mTaskOverView = new HashMap<>(); /** * {@link Task} belonging to this application, most recently resumed * task at front. */ private ArrayDeque mTaskOrdering = new ArrayDeque<>(); /** * Listeners that get notified whenever the tasks get modified. * This also includes reordering of activities within the task. */ private List>> mListeners = new ArrayList<>(); /** * When an {@link Activity} becomes resumed, it should be put at the top within it's task. * Furthermore the task it belongs to should become the most recent task. * * We also check if any {@link Activity} we have thinks it's {@link Activity#getTaskId()} * does not correspond to the {@link Task} we associated it to. * If so we move them to the {@link Task} they report they should belong to. * * @param activity the {@link Activity} that has been resumed. */ public synchronized void onResume(Activity activity) { logNameEventAndTask(activity, "onResume"); int id = activity.getTaskId(); Task task = getOrCreateTask(mTaskOverView, id); task.activityResumed(activity); bringToFront(task); checkForMovedActivities().ifPresent(this::moveActivitiesInOrder); notifyListeners(); } /** * When an {@link Activity} is being destroyed, we remove it from the task it is in. * If this activity was the last activity in the task, we also remove the * {@link Task}. * * @param activity the {@link Activity} that has been resumed. */ public synchronized void onDestroy(Activity activity) { logNameEventAndTask(activity, "onDestroy"); // Find the activity by identity in case it has been moved. Optional existingTask = mTaskOverView.values().stream() .filter(t -> t.containsActivity(activity)) .findAny(); if (existingTask.isPresent()) { Task task = existingTask.get(); task.activityDestroyed(activity); // If this was the last activity in the task, remove it. if (task.mActivities.isEmpty()) { mTaskOverView.remove(task.id); mTaskOrdering.remove(task); } } notifyListeners(); } // If it's not already at the front of the queue, remove it and add it at the front. private void bringToFront(Task task) { if (mTaskOrdering.peekFirst() != task) { mTaskOrdering.remove(task); mTaskOrdering.addFirst(task); } } // Check if there is a task that has activities that belong to another task. private Optional checkForMovedActivities() { for (Task task : mTaskOverView.values()) { for (Activity activity : task.mActivities) { if (activity.getTaskId() != task.id) { return Optional.of(task); } } } return Optional.empty(); } // When a task contains activities that belong to another task, we move them // to the other task, in the same order they had in the current task. private void moveActivitiesInOrder(Task task) { Iterator iterator = task.mActivities.iterator(); while (iterator.hasNext()) { Activity activity = iterator.next(); int id = activity.getTaskId(); if (id != task.id) { Task target = mTaskOverView.get(id); //the task the activity moved to was not yet known if (target == null) { Task newTask = Task.newTask(id); mTaskOverView.put(id, newTask); // we're not sure where this task should belong now // we put it behind the current front task putBehindFront(newTask); target = newTask; } target.mActivities.add(activity); iterator.remove(); } } } // If activities moved to a new task that we don't know about yet, we put it behind // the most recent task. private void putBehindFront(Task task) { Task first = mTaskOrdering.removeFirst(); mTaskOrdering.addFirst(task); mTaskOrdering.addFirst(first); } public static void logNameEventAndTask(Activity activity, String event) { Log.i(TAG, activity.getClass().getSimpleName() + " " + event + "task id: " + activity.getTaskId()); } public synchronized int size() { return mTaskOverView.size(); } private synchronized void notifyListeners() { List tasks = mTaskOrdering.stream().map(Task::copyForUi).collect(toList()); for (Consumer> listener : mListeners) { listener.accept(tasks); } } public synchronized void addListener(Consumer> listener) { mListeners.add(listener); } public synchronized void removeListener(Consumer> listener) { mListeners.remove(listener); } } private static Task getOrCreateTask(Map map, int id) { Task backup = Task.newTask(id); Task task = map.putIfAbsent(id, backup); if (task == null) { return backup; } else { return task; } } static class Task { public final int id; /** * The activities in this task, * element 0 being the least recent and the last element being the most recent */ protected final List mActivities; Task(int id, List activities) { this.id = id; mActivities = activities; } static Task newTask(int id) { return new Task(id, new ArrayList<>()); } public void activityResumed(Activity activity) { ensureSameTask(activity); Iterator activityIterator = mActivities.iterator(); while (activityIterator.hasNext()) { Activity next = activityIterator.next(); //the activity is being moved up. if (next == activity) { activityIterator.remove(); break; } } mActivities.add(activity); } public boolean containsActivity(Activity activity) { for (Activity activity1 : mActivities) { if (activity1 == activity) { return true; } } return false; } private void ensureSameTask(Activity activity) { if (activity.getTaskId() != id) { throw new RuntimeException("adding activity to task with different id"); } } public void activityDestroyed(Activity activity) { ensureSameTask(activity); mActivities.removeIf(a -> a == activity); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Task task = (Task) o; return id == task.id; } @Override public int hashCode() { return Objects.hash(id); } @Override public String toString() { return "Task{" + "id=" + id + ", mActivities=" + mActivities + '}'; } public static Task copyForUi(Task task) { return new Task(task.id, reverseAndCopy(task.mActivities)); } public static List reverseAndCopy(List ts) { ListIterator iterator = ts.listIterator(ts.size()); List result = new ArrayList<>(); while (iterator.hasPrevious()) { result.add(iterator.previous()); } return result; } } }