1 /*
2  * Copyright (C) 2023 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 android.app.servertransaction;
18 
19 import static android.app.WindowConfiguration.areConfigurationsEqualForDisplay;
20 import static android.view.Display.INVALID_DISPLAY;
21 
22 import static com.android.window.flags.Flags.activityWindowInfoFlag;
23 import static com.android.window.flags.Flags.bundleClientTransactionFlag;
24 
25 import static java.util.Objects.requireNonNull;
26 
27 import android.annotation.NonNull;
28 import android.app.Activity;
29 import android.app.ActivityThread;
30 import android.content.Context;
31 import android.content.res.Configuration;
32 import android.hardware.display.DisplayManagerGlobal;
33 import android.os.IBinder;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 import android.util.Log;
37 import android.window.ActivityWindowInfo;
38 
39 import com.android.internal.annotations.GuardedBy;
40 import com.android.internal.annotations.VisibleForTesting;
41 
42 import java.util.concurrent.RejectedExecutionException;
43 import java.util.function.BiConsumer;
44 
45 /**
46  * Singleton controller to manage listeners to individual {@link ClientTransaction}.
47  *
48  * @hide
49  */
50 public class ClientTransactionListenerController {
51 
52     private static final String TAG = "ClientTransactionListenerController";
53 
54     private static ClientTransactionListenerController sController;
55 
56     private final Object mLock = new Object();
57     private final DisplayManagerGlobal mDisplayManager;
58 
59     /** Listeners registered via {@link #registerActivityWindowInfoChangedListener(BiConsumer)}. */
60     @GuardedBy("mLock")
61     private final ArraySet<BiConsumer<IBinder, ActivityWindowInfo>>
62             mActivityWindowInfoChangedListeners = new ArraySet<>();
63 
64     /**
65      * Keeps track of the Context whose Configuration will get updated, mapping to the config before
66      * the change.
67      */
68     @GuardedBy("mLock")
69     private final ArrayMap<Context, Configuration> mContextToPreChangedConfigMap = new ArrayMap<>();
70 
71     /** Whether there is an {@link ClientTransaction} being executed. */
72     @GuardedBy("mLock")
73     private boolean mIsClientTransactionExecuting;
74 
75     /** Gets the singleton controller. */
76     @NonNull
getInstance()77     public static ClientTransactionListenerController getInstance() {
78         synchronized (ClientTransactionListenerController.class) {
79             if (sController == null) {
80                 sController = new ClientTransactionListenerController(
81                         DisplayManagerGlobal.getInstance());
82             }
83             return sController;
84         }
85     }
86 
87     /** Creates a new instance for test only. */
88     @VisibleForTesting
89     @NonNull
createInstanceForTesting( @onNull DisplayManagerGlobal displayManager)90     public static ClientTransactionListenerController createInstanceForTesting(
91             @NonNull DisplayManagerGlobal displayManager) {
92         return new ClientTransactionListenerController(displayManager);
93     }
94 
ClientTransactionListenerController(@onNull DisplayManagerGlobal displayManager)95     private ClientTransactionListenerController(@NonNull DisplayManagerGlobal displayManager) {
96         mDisplayManager = requireNonNull(displayManager);
97     }
98 
99     /**
100      * Registers to listen on activity {@link ActivityWindowInfo} change.
101      * The listener will be invoked with two parameters: {@link Activity#getActivityToken()} and
102      * {@link ActivityWindowInfo}.
103      */
registerActivityWindowInfoChangedListener( @onNull BiConsumer<IBinder, ActivityWindowInfo> listener)104     public void registerActivityWindowInfoChangedListener(
105             @NonNull BiConsumer<IBinder, ActivityWindowInfo> listener) {
106         if (!activityWindowInfoFlag()) {
107             return;
108         }
109         synchronized (mLock) {
110             mActivityWindowInfoChangedListeners.add(listener);
111         }
112     }
113 
114     /**
115      * Unregisters the listener that was previously registered via
116      * {@link #registerActivityWindowInfoChangedListener(BiConsumer)}
117      */
unregisterActivityWindowInfoChangedListener( @onNull BiConsumer<IBinder, ActivityWindowInfo> listener)118     public void unregisterActivityWindowInfoChangedListener(
119             @NonNull BiConsumer<IBinder, ActivityWindowInfo> listener) {
120         if (!activityWindowInfoFlag()) {
121             return;
122         }
123         synchronized (mLock) {
124             mActivityWindowInfoChangedListeners.remove(listener);
125         }
126     }
127 
128     /**
129      * Called when receives a {@link ClientTransaction} that is updating an activity's
130      * {@link ActivityWindowInfo}.
131      */
onActivityWindowInfoChanged(@onNull IBinder activityToken, @NonNull ActivityWindowInfo activityWindowInfo)132     public void onActivityWindowInfoChanged(@NonNull IBinder activityToken,
133             @NonNull ActivityWindowInfo activityWindowInfo) {
134         if (!activityWindowInfoFlag()) {
135             return;
136         }
137         final Object[] activityWindowInfoChangedListeners;
138         synchronized (mLock) {
139             if (mActivityWindowInfoChangedListeners.isEmpty()) {
140                 return;
141             }
142             activityWindowInfoChangedListeners = mActivityWindowInfoChangedListeners.toArray();
143         }
144         for (Object activityWindowInfoChangedListener : activityWindowInfoChangedListeners) {
145             ((BiConsumer<IBinder, ActivityWindowInfo>) activityWindowInfoChangedListener)
146                     .accept(activityToken, new ActivityWindowInfo(activityWindowInfo));
147         }
148     }
149 
150     /** Called when starts executing a remote {@link ClientTransaction}. */
onClientTransactionStarted()151     public void onClientTransactionStarted() {
152         synchronized (mLock) {
153             mIsClientTransactionExecuting = true;
154         }
155     }
156 
157     /** Called when finishes executing a remote {@link ClientTransaction}. */
onClientTransactionFinished()158     public void onClientTransactionFinished() {
159         final ArraySet<Integer> configUpdatedDisplayIds;
160         synchronized (mLock) {
161             mIsClientTransactionExecuting = false;
162 
163             // When {@link Configuration} is changed, we want to trigger display change callback as
164             // well, because Display reads some fields from {@link Configuration}.
165             if (mContextToPreChangedConfigMap.isEmpty()) {
166                 return;
167             }
168 
169             // Calculate display ids that have config changed.
170             configUpdatedDisplayIds = new ArraySet<>();
171             final int contextCount = mContextToPreChangedConfigMap.size();
172             try {
173                 for (int i = 0; i < contextCount; i++) {
174                     final Context context = mContextToPreChangedConfigMap.keyAt(i);
175                     final Configuration preChangedConfig = mContextToPreChangedConfigMap.valueAt(i);
176                     if (shouldReportDisplayChange(context, preChangedConfig)) {
177                         configUpdatedDisplayIds.add(context.getDisplayId());
178                     }
179                 }
180             } finally {
181                 mContextToPreChangedConfigMap.clear();
182             }
183         }
184 
185         // Dispatch the display changed callbacks.
186         try {
187             final int displayCount = configUpdatedDisplayIds.size();
188             for (int i = 0; i < displayCount; i++) {
189                 final int displayId = configUpdatedDisplayIds.valueAt(i);
190                 onDisplayChanged(displayId);
191             }
192         } catch (RejectedExecutionException e) {
193             Log.w(TAG, "Failed to notify DisplayListener because the Handler is shutting down");
194         }
195     }
196 
197     /** Called before updating the Configuration of the given {@code context}. */
onContextConfigurationPreChanged(@onNull Context context)198     public void onContextConfigurationPreChanged(@NonNull Context context) {
199         if (!bundleClientTransactionFlag() || ActivityThread.isSystem()) {
200             // Not enable for system server.
201             return;
202         }
203         synchronized (mLock) {
204             if (mContextToPreChangedConfigMap.containsKey(context)) {
205                 // There is an earlier change that hasn't been reported yet.
206                 return;
207             }
208             mContextToPreChangedConfigMap.put(context,
209                     new Configuration(context.getResources().getConfiguration()));
210         }
211     }
212 
213     /** Called after updating the Configuration of the given {@code context}. */
onContextConfigurationPostChanged(@onNull Context context)214     public void onContextConfigurationPostChanged(@NonNull Context context) {
215         if (!bundleClientTransactionFlag() || ActivityThread.isSystem()) {
216             // Not enable for system server.
217             return;
218         }
219         int changedDisplayId = INVALID_DISPLAY;
220         synchronized (mLock) {
221             if (mIsClientTransactionExecuting) {
222                 // Wait until #onClientTransactionFinished to prevent it from triggering the same
223                 // #onDisplayChanged multiple times within the same ClientTransaction.
224                 return;
225             }
226             final Configuration preChangedConfig = mContextToPreChangedConfigMap.remove(context);
227             if (preChangedConfig != null && shouldReportDisplayChange(context, preChangedConfig)) {
228                 changedDisplayId = context.getDisplayId();
229             }
230         }
231 
232         if (changedDisplayId != INVALID_DISPLAY) {
233             try {
234                 onDisplayChanged(changedDisplayId);
235             } catch (RejectedExecutionException e) {
236                 Log.w(TAG, "Failed to notify DisplayListener because the Handler is shutting down");
237             }
238         }
239     }
240 
shouldReportDisplayChange(@onNull Context context, @NonNull Configuration preChangedConfig)241     private boolean shouldReportDisplayChange(@NonNull Context context,
242             @NonNull Configuration preChangedConfig) {
243         final Configuration postChangedConfig = context.getResources().getConfiguration();
244         return !areConfigurationsEqualForDisplay(postChangedConfig, preChangedConfig);
245     }
246 
247     /**
248      * Called when receives a {@link Configuration} changed event that is updating display-related
249      * window configuration.
250      *
251      * @throws RejectedExecutionException if the display listener handler is closing.
252      */
253     @VisibleForTesting
onDisplayChanged(int displayId)254     public void onDisplayChanged(int displayId) throws RejectedExecutionException {
255         mDisplayManager.handleDisplayChangeFromWindowManager(displayId);
256     }
257 }
258