Since adding a public api for the playground app was problematic, we now keep track of onResume and onDestroy calls globally inside the app. This fixes a few bugs in cases where tasks would be hidden from the ActivityManger#.getAppTasks() method and allows us to listen for changes allowing us to do without a periodic refresh of the UI. Test: Manual Change-Id: Idb834cbdc7aad6442dd962c95b5321e1d75695fe
286 lines
9.6 KiB
Java
286 lines
9.6 KiB
Java
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<Integer, Task> mTaskOverView = new HashMap<>();
|
|
|
|
/**
|
|
* {@link Task} belonging to this application, most recently resumed
|
|
* task at front.
|
|
*/
|
|
private ArrayDeque<Task> mTaskOrdering = new ArrayDeque<>();
|
|
|
|
/**
|
|
* Listeners that get notified whenever the tasks get modified.
|
|
* This also includes reordering of activities within the task.
|
|
*/
|
|
private List<Consumer<List<Task>>> 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<Task> 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<Task> 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<Activity> 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<Task> tasks = mTaskOrdering.stream().map(Task::copyForUi).collect(toList());
|
|
|
|
for (Consumer<List<Task>> listener : mListeners) {
|
|
listener.accept(tasks);
|
|
}
|
|
}
|
|
|
|
public synchronized void addListener(Consumer<List<Task>> listener) {
|
|
mListeners.add(listener);
|
|
}
|
|
|
|
public synchronized void removeListener(Consumer<List<Task>> listener) {
|
|
mListeners.remove(listener);
|
|
}
|
|
}
|
|
|
|
private static Task getOrCreateTask(Map<Integer, Task> 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<Activity> mActivities;
|
|
|
|
|
|
Task(int id, List<Activity> 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<Activity> 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 <T> List<T> reverseAndCopy(List<T> ts) {
|
|
ListIterator<T> iterator = ts.listIterator(ts.size());
|
|
List<T> result = new ArrayList<>();
|
|
|
|
while (iterator.hasPrevious()) {
|
|
result.add(iterator.previous());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|