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