• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.transition;
18 
19 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
20 import static android.view.Display.DEFAULT_DISPLAY;
21 import static android.view.Display.INVALID_DISPLAY;
22 import static android.view.WindowManager.TRANSIT_CHANGE;
23 import static android.view.WindowManager.TRANSIT_OPEN;
24 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
25 import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
26 
27 import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
28 import static com.android.wm.shell.transition.Transitions.TransitionObserver;
29 
30 import android.annotation.NonNull;
31 import android.app.ActivityManager.RunningTaskInfo;
32 import android.os.RemoteException;
33 import android.util.ArraySet;
34 import android.util.IndentingPrintWriter;
35 import android.util.Slog;
36 import android.util.SparseArray;
37 import android.window.TransitionInfo;
38 
39 import com.android.wm.shell.shared.FocusTransitionListener;
40 import com.android.wm.shell.shared.IFocusTransitionListener;
41 
42 import java.io.PrintWriter;
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.concurrent.Executor;
47 
48 /**
49  * The {@link TransitionObserver} that observes for transitions involving focus switch.
50  * It reports transitions to callers outside of the process via {@link IFocusTransitionListener},
51  * and callers within the process via {@link FocusTransitionListener}.
52  */
53 public class FocusTransitionObserver {
54     private static final String TAG = FocusTransitionObserver.class.getSimpleName();
55 
56     private IFocusTransitionListener mRemoteListener;
57     private final Map<FocusTransitionListener, Executor> mLocalListeners =
58             new HashMap<>();
59 
60     private int mFocusedDisplayId = DEFAULT_DISPLAY;
61     private final SparseArray<RunningTaskInfo> mFocusedTaskOnDisplay = new SparseArray<>();
62 
63     private final ArraySet<RunningTaskInfo> mTmpTasksToBeNotified = new ArraySet<>();
64 
FocusTransitionObserver()65     public FocusTransitionObserver() {}
66 
67     /**
68      * Update display/window focus state from the given transition info and notifies changes if any.
69      */
updateFocusState(@onNull TransitionInfo info)70     public void updateFocusState(@NonNull TransitionInfo info) {
71         if (!enableDisplayFocusInShellTransitions()) {
72             return;
73         }
74         final SparseArray<RunningTaskInfo> lastTransitionFocusedTasks =
75                 mFocusedTaskOnDisplay.clone();
76 
77         final List<TransitionInfo.Change> changes = info.getChanges();
78         for (int i = changes.size() - 1; i >= 0; i--) {
79             final TransitionInfo.Change change = changes.get(i);
80 
81             final RunningTaskInfo task = change.getTaskInfo();
82             if (task != null) {
83                 if (change.hasFlags(FLAG_MOVED_TO_TOP) || change.getMode() == TRANSIT_OPEN) {
84                     updateFocusedTaskPerDisplay(task, task.displayId);
85                 } else {
86                     // Update focus assuming that any task moved to another display is focused in
87                     // the new display.
88                     // TODO(sahok): remove this logic when b/388665104 is fixed
89                     final boolean isBeyondDisplay = change.getStartDisplayId() != INVALID_DISPLAY
90                             && change.getEndDisplayId() != INVALID_DISPLAY
91                             && change.getStartDisplayId() != change.getEndDisplayId();
92 
93                     RunningTaskInfo lastTransitionFocusedTaskOnStartDisplay =
94                             lastTransitionFocusedTasks.get(change.getStartDisplayId());
95                     final boolean isLastTransitionFocused =
96                             lastTransitionFocusedTaskOnStartDisplay != null
97                                     && task.taskId
98                                             == lastTransitionFocusedTaskOnStartDisplay.taskId;
99                     if (change.getMode() == TRANSIT_CHANGE && isBeyondDisplay
100                             && isLastTransitionFocused) {
101                         // The task have moved to another display and keeps its focus.
102                         // MOVE_TO_TOP is not reported but we need to update the focused task in
103                         // the end display.
104                         updateFocusedTaskPerDisplay(task, change.getEndDisplayId());
105                     }
106                 }
107             }
108 
109 
110             if (change.hasFlags(FLAG_IS_DISPLAY) && change.hasFlags(FLAG_MOVED_TO_TOP)) {
111                 if (mFocusedDisplayId != change.getEndDisplayId()) {
112                     updateFocusedDisplay(change.getEndDisplayId());
113                 }
114             }
115         }
116         mTmpTasksToBeNotified.forEach(this::notifyTaskFocusChanged);
117         mTmpTasksToBeNotified.clear();
118     }
119 
updateFocusedTaskPerDisplay(RunningTaskInfo task, int displayId)120     private void updateFocusedTaskPerDisplay(RunningTaskInfo task, int displayId) {
121         final RunningTaskInfo lastFocusedTaskOnDisplay =
122                 mFocusedTaskOnDisplay.get(displayId);
123         if (lastFocusedTaskOnDisplay != null) {
124             mTmpTasksToBeNotified.add(lastFocusedTaskOnDisplay);
125         }
126         mTmpTasksToBeNotified.add(task);
127         mFocusedTaskOnDisplay.put(displayId, task);
128     }
129 
updateFocusedDisplay(int endDisplayId)130     private void updateFocusedDisplay(int endDisplayId) {
131         final RunningTaskInfo lastGloballyFocusedTask =
132                 mFocusedTaskOnDisplay.get(mFocusedDisplayId);
133         if (lastGloballyFocusedTask != null) {
134             mTmpTasksToBeNotified.add(lastGloballyFocusedTask);
135         }
136         mFocusedDisplayId = endDisplayId;
137         notifyFocusedDisplayChanged();
138         final RunningTaskInfo currentGloballyFocusedTask =
139                 mFocusedTaskOnDisplay.get(mFocusedDisplayId);
140         if (currentGloballyFocusedTask != null) {
141             mTmpTasksToBeNotified.add(currentGloballyFocusedTask);
142         }
143     }
144 
145     /**
146      * Sets the focus transition listener that receives any transitions resulting in focus switch.
147      * This is for calls from outside the Shell, within the host process.
148      *
149      */
setLocalFocusTransitionListener(FocusTransitionListener listener, Executor executor)150     public void setLocalFocusTransitionListener(FocusTransitionListener listener,
151             Executor executor) {
152         if (!enableDisplayFocusInShellTransitions()) {
153             return;
154         }
155         mLocalListeners.put(listener, executor);
156         executor.execute(() -> {
157             listener.onFocusedDisplayChanged(mFocusedDisplayId);
158             mTmpTasksToBeNotified.forEach(this::notifyTaskFocusChanged);
159         });
160     }
161 
162     /**
163      * Sets the focus transition listener that receives any transitions resulting in focus switch.
164      * This is for calls from outside the Shell, within the host process.
165      *
166      */
unsetLocalFocusTransitionListener(FocusTransitionListener listener)167     public void unsetLocalFocusTransitionListener(FocusTransitionListener listener) {
168         if (!enableDisplayFocusInShellTransitions()) {
169             return;
170         }
171         mLocalListeners.remove(listener);
172     }
173 
174     /**
175      * Sets the focus transition listener that receives any transitions resulting in focus switch.
176      * This is for calls from outside the host process.
177      */
setRemoteFocusTransitionListener(Transitions transitions, IFocusTransitionListener listener)178     public void setRemoteFocusTransitionListener(Transitions transitions,
179             IFocusTransitionListener listener) {
180         if (!enableDisplayFocusInShellTransitions()) {
181             return;
182         }
183         mRemoteListener = listener;
184         notifyFocusedDisplayChangedToRemote();
185     }
186 
notifyTaskFocusChanged(RunningTaskInfo task)187     private void notifyTaskFocusChanged(RunningTaskInfo task) {
188         final boolean isFocusedOnDisplay = isFocusedOnDisplay(task);
189         final boolean isFocusedGlobally = hasGlobalFocus(task);
190         mLocalListeners.forEach((listener, executor) ->
191                 executor.execute(() -> listener.onFocusedTaskChanged(task.taskId,
192                         isFocusedOnDisplay, isFocusedGlobally)));
193     }
194 
notifyFocusedDisplayChanged()195     private void notifyFocusedDisplayChanged() {
196         notifyFocusedDisplayChangedToRemote();
197         mLocalListeners.forEach((listener, executor) ->
198                 executor.execute(() -> {
199                     listener.onFocusedDisplayChanged(mFocusedDisplayId);
200                 }));
201     }
202 
notifyFocusedDisplayChangedToRemote()203     private void notifyFocusedDisplayChangedToRemote() {
204         if (mRemoteListener != null) {
205             try {
206                 mRemoteListener.onFocusedDisplayChanged(mFocusedDisplayId);
207             } catch (RemoteException e) {
208                 Slog.w(TAG, "Failed call notifyFocusedDisplayChangedToRemote", e);
209             }
210         }
211     }
212 
isFocusedOnDisplay(@onNull RunningTaskInfo task)213     private boolean isFocusedOnDisplay(@NonNull RunningTaskInfo task) {
214         if (!enableDisplayFocusInShellTransitions()) {
215             return task.isFocused;
216         }
217         final RunningTaskInfo focusedTaskOnDisplay = mFocusedTaskOnDisplay.get(task.displayId);
218         return focusedTaskOnDisplay != null && focusedTaskOnDisplay.taskId == task.taskId;
219     }
220 
221     /**
222      * Gets the globally focused task ID.
223      */
getGloballyFocusedTaskId()224     public int getGloballyFocusedTaskId() {
225         if (!enableDisplayFocusInShellTransitions() || mFocusedDisplayId == INVALID_DISPLAY) {
226             return INVALID_TASK_ID;
227         }
228         final RunningTaskInfo globallyFocusedTask = mFocusedTaskOnDisplay.get(mFocusedDisplayId);
229         return globallyFocusedTask != null ? globallyFocusedTask.taskId : INVALID_TASK_ID;
230     }
231 
232     /**
233      * Checks whether the given task has focused globally on the system.
234      * (Note {@link RunningTaskInfo#isFocused} represents per-display focus.)
235      */
hasGlobalFocus(@onNull RunningTaskInfo task)236     public boolean hasGlobalFocus(@NonNull RunningTaskInfo task) {
237         if (!enableDisplayFocusInShellTransitions()) {
238             return task.isFocused;
239         }
240         return task.displayId == mFocusedDisplayId && isFocusedOnDisplay(task);
241     }
242 
243     /** Dumps focused display and tasks. */
dump(PrintWriter originalWriter, String prefix)244     public void dump(PrintWriter originalWriter, String prefix) {
245         final IndentingPrintWriter writer =
246                 new IndentingPrintWriter(originalWriter, "    ", prefix);
247         writer.println("FocusTransitionObserver:");
248         writer.increaseIndent();
249         writer.printf("currentFocusedDisplayId=%d\n", mFocusedDisplayId);
250         writer.println("currentFocusedTaskOnDisplay:");
251         writer.increaseIndent();
252         for (int i = 0; i < mFocusedTaskOnDisplay.size(); i++) {
253             writer.printf("Display #%d: taskId=%d topActivity=%s\n",
254                     mFocusedTaskOnDisplay.keyAt(i),
255                     mFocusedTaskOnDisplay.valueAt(i).taskId,
256                     mFocusedTaskOnDisplay.valueAt(i).topActivity);
257         }
258     }
259 }
260