1 package com.example.android.intentplayground;
2 
3 import static java.util.stream.Collectors.toList;
4 
5 import android.app.Activity;
6 import android.util.Log;
7 
8 import java.util.ArrayDeque;
9 import java.util.ArrayList;
10 import java.util.HashMap;
11 import java.util.Iterator;
12 import java.util.List;
13 import java.util.ListIterator;
14 import java.util.Map;
15 import java.util.Objects;
16 import java.util.Optional;
17 import java.util.function.Consumer;
18 
19 /**
20  * Provides information about the current runnings tasks and activities in the system, by tracking
21  * all the lifecycle events happening in the app using {@link Tracker}. {@link Tracker} can be
22  * observed for changes in this state. Information regarding the order of activities is kept inside
23  * {@link Task}.
24  */
25 public class Tracking {
26 
27     /**
28      * Stores the {@link com.android.server.wm.Task}-s in MRU order together with the activities
29      * within that task and their order. Classes can be notified of changes in this state through
30      * {@link Tracker#addListener(Consumer)}
31      */
32     public static class Tracker {
33         private static final String TAG = "Tracker";
34 
35         /**
36          * Stores {@link Task} by their id.
37          */
38         private HashMap<Integer, Task> mTaskOverView = new HashMap<>();
39 
40         /**
41          * {@link Task} belonging to this application, most recently resumed
42          * task at front.
43          */
44         private ArrayDeque<Task> mTaskOrdering = new ArrayDeque<>();
45 
46         /**
47          * Listeners that get notified whenever the tasks get modified.
48          * This also includes reordering of activities within the task.
49          */
50         private List<Consumer<List<Task>>> mListeners = new ArrayList<>();
51 
52         /**
53          * When an {@link Activity} becomes resumed, it should be put at the top within it's task.
54          * Furthermore the task it belongs to should become the most recent task.
55          *
56          * We also check if any {@link Activity} we have thinks it's {@link Activity#getTaskId()}
57          * does not correspond to the {@link Task} we associated it to.
58          * If so we move them to the {@link Task} they report they should belong to.
59          *
60          * @param activity the {@link Activity} that has been resumed.
61          */
onResume(Activity activity)62         public synchronized void onResume(Activity activity) {
63             logNameEventAndTask(activity, "onResume");
64 
65             int id = activity.getTaskId();
66             Task task = getOrCreateTask(mTaskOverView, id);
67             task.activityResumed(activity);
68             bringToFront(task);
69 
70             checkForMovedActivities().ifPresent(this::moveActivitiesInOrder);
71 
72             notifyListeners();
73         }
74 
75         /**
76          * When an {@link Activity} is being destroyed, we remove it from the task it is in.
77          * If this activity was the last activity in the task, we also remove the
78          * {@link Task}.
79          *
80          * @param activity the {@link Activity} that has been resumed.
81          */
onDestroy(Activity activity)82         public synchronized void onDestroy(Activity activity) {
83             logNameEventAndTask(activity, "onDestroy");
84 
85             // Find the activity by identity in case it has been moved.
86             Optional<Task> existingTask = mTaskOverView.values().stream()
87                     .filter(t -> t.containsActivity(activity))
88                     .findAny();
89 
90             if (existingTask.isPresent()) {
91                 Task task = existingTask.get();
92                 task.activityDestroyed(activity);
93 
94                 // If this was the last activity in the task, remove it.
95                 if (task.mActivities.isEmpty()) {
96                     mTaskOverView.remove(task.id);
97                     mTaskOrdering.remove(task);
98                 }
99             }
100 
101             notifyListeners();
102         }
103 
104         // If it's not already at the front of the queue, remove it and add it at the front.
bringToFront(Task task)105         private void bringToFront(Task task) {
106             if (mTaskOrdering.peekFirst() != task) {
107                 mTaskOrdering.remove(task);
108                 mTaskOrdering.addFirst(task);
109             }
110         }
111 
112         // Check if there is a task that has activities that belong to another task.
checkForMovedActivities()113         private Optional<Task> checkForMovedActivities() {
114             for (Task task : mTaskOverView.values()) {
115                 for (Activity activity : task.mActivities) {
116                     if (activity.getTaskId() != task.id) {
117                         return Optional.of(task);
118                     }
119                 }
120             }
121             return Optional.empty();
122         }
123 
124         // When a task contains activities that belong to another task, we move them
125         // to the other task, in the same order they had in the current task.
moveActivitiesInOrder(Task task)126         private void moveActivitiesInOrder(Task task) {
127             Iterator<Activity> iterator = task.mActivities.iterator();
128             while (iterator.hasNext()) {
129                 Activity activity = iterator.next();
130                 int id = activity.getTaskId();
131                 if (id != task.id) {
132                     Task target = mTaskOverView.get(id);
133                     //the task the activity moved to was not yet known
134                     if (target == null) {
135                         Task newTask = Task.newTask(id);
136                         mTaskOverView.put(id, newTask);
137                         // we're not sure where this task should belong now
138                         // we put it behind the current front task
139                         putBehindFront(newTask);
140                         target = newTask;
141                     }
142                     target.mActivities.add(activity);
143                     iterator.remove();
144                 }
145             }
146         }
147 
148         // If activities moved to a new task that we don't know about yet, we put it behind
149         // the most recent task.
putBehindFront(Task task)150         private void putBehindFront(Task task) {
151             Task first = mTaskOrdering.removeFirst();
152             mTaskOrdering.addFirst(task);
153             mTaskOrdering.addFirst(first);
154         }
155 
156 
logNameEventAndTask(Activity activity, String event)157         public static void logNameEventAndTask(Activity activity, String event) {
158             Log.i(TAG, activity.getClass().getSimpleName() + " " + event + "task id: "
159                     + activity.getTaskId());
160         }
161 
size()162         public synchronized int size() {
163             return mTaskOverView.size();
164         }
165 
notifyListeners()166         private synchronized void notifyListeners() {
167             List<Task> tasks = mTaskOrdering.stream().map(Task::copyForUi).collect(toList());
168 
169             for (Consumer<List<Task>> listener : mListeners) {
170                 listener.accept(tasks);
171             }
172         }
173 
addListener(Consumer<List<Task>> listener)174         public synchronized void addListener(Consumer<List<Task>> listener) {
175             mListeners.add(listener);
176         }
177 
removeListener(Consumer<List<Task>> listener)178         public synchronized void removeListener(Consumer<List<Task>> listener) {
179             mListeners.remove(listener);
180         }
181     }
182 
getOrCreateTask(Map<Integer, Task> map, int id)183     private static Task getOrCreateTask(Map<Integer, Task> map, int id) {
184         Task backup = Task.newTask(id);
185         Task task = map.putIfAbsent(id, backup);
186         if (task == null) {
187             return backup;
188         } else {
189             return task;
190         }
191     }
192 
193     static class Task {
194         public final int id;
195         /**
196          * The activities in this task,
197          * element 0 being the least recent and the last element being the most recent
198          */
199         protected final List<Activity> mActivities;
200 
201 
Task(int id, List<Activity> activities)202         Task(int id, List<Activity> activities) {
203             this.id = id;
204             mActivities = activities;
205         }
206 
newTask(int id)207         static Task newTask(int id) {
208             return new Task(id, new ArrayList<>());
209         }
210 
211 
activityResumed(Activity activity)212         public void activityResumed(Activity activity) {
213             ensureSameTask(activity);
214 
215             Iterator<Activity> activityIterator = mActivities.iterator();
216             while (activityIterator.hasNext()) {
217                 Activity next = activityIterator.next();
218                 //the activity is being moved up.
219                 if (next == activity) {
220                     activityIterator.remove();
221                     break;
222                 }
223             }
224 
225             mActivities.add(activity);
226         }
227 
containsActivity(Activity activity)228         public boolean containsActivity(Activity activity) {
229             for (Activity activity1 : mActivities) {
230                 if (activity1 == activity) {
231                     return true;
232                 }
233             }
234 
235             return false;
236         }
237 
ensureSameTask(Activity activity)238         private void ensureSameTask(Activity activity) {
239             if (activity.getTaskId() != id) {
240                 throw new RuntimeException("adding activity to task with different id");
241             }
242         }
243 
activityDestroyed(Activity activity)244         public void activityDestroyed(Activity activity) {
245             ensureSameTask(activity);
246             mActivities.removeIf(a -> a == activity);
247         }
248 
249         @Override
equals(Object o)250         public boolean equals(Object o) {
251             if (this == o) return true;
252             if (o == null || getClass() != o.getClass()) return false;
253             Task task = (Task) o;
254             return id == task.id;
255         }
256 
257         @Override
hashCode()258         public int hashCode() {
259             return Objects.hash(id);
260         }
261 
262         @Override
toString()263         public String toString() {
264             return "Task{" +
265                     "id=" + id +
266                     ", mActivities=" + mActivities +
267                     '}';
268         }
269 
copyForUi(Task task)270         public static Task copyForUi(Task task) {
271             return new Task(task.id, reverseAndCopy(task.mActivities));
272         }
273 
reverseAndCopy(List<T> ts)274         public static <T> List<T> reverseAndCopy(List<T> ts) {
275             ListIterator<T> iterator = ts.listIterator(ts.size());
276             List<T> result = new ArrayList<>();
277 
278             while (iterator.hasPrevious()) {
279                 result.add(iterator.previous());
280             }
281 
282             return result;
283         }
284     }
285 }
286