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