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