1 /* 2 * Copyright (C) 2021 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 androidx.window.extensions.layout; 18 19 import static android.view.Display.DEFAULT_DISPLAY; 20 21 import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT; 22 import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED; 23 import static androidx.window.util.ExtensionHelper.isZero; 24 import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; 25 import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; 26 27 import android.app.Activity; 28 import android.app.ActivityThread; 29 import android.app.Application; 30 import android.app.WindowConfiguration; 31 import android.content.ComponentCallbacks; 32 import android.content.Context; 33 import android.content.res.Configuration; 34 import android.graphics.Rect; 35 import android.os.Bundle; 36 import android.os.IBinder; 37 import android.util.ArrayMap; 38 import android.util.Log; 39 40 import androidx.annotation.GuardedBy; 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.annotation.UiContext; 44 import androidx.window.common.CommonFoldingFeature; 45 import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; 46 import androidx.window.common.EmptyLifecycleCallbacksAdapter; 47 import androidx.window.extensions.core.util.function.Consumer; 48 import androidx.window.extensions.util.DeduplicateConsumer; 49 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.List; 53 import java.util.Map; 54 import java.util.Set; 55 56 /** 57 * Reference implementation of androidx.window.extensions.layout OEM interface for use with 58 * WindowManager Jetpack. 59 */ 60 public class WindowLayoutComponentImpl implements WindowLayoutComponent { 61 private static final String TAG = WindowLayoutComponentImpl.class.getSimpleName(); 62 63 private final Object mLock = new Object(); 64 65 @GuardedBy("mLock") 66 private final Map<Context, DeduplicateConsumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = 67 new ArrayMap<>(); 68 69 @GuardedBy("mLock") 70 private final DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer; 71 72 @GuardedBy("mLock") 73 private final List<CommonFoldingFeature> mLastReportedFoldingFeatures = new ArrayList<>(); 74 75 @GuardedBy("mLock") 76 private final Map<IBinder, ConfigurationChangeListener> mConfigurationChangeListeners = 77 new ArrayMap<>(); 78 79 @GuardedBy("mLock") 80 private final Map<java.util.function.Consumer<WindowLayoutInfo>, Consumer<WindowLayoutInfo>> 81 mJavaToExtConsumers = new ArrayMap<>(); 82 83 private final RawConfigurationChangedListener mRawConfigurationChangedListener = 84 new RawConfigurationChangedListener(); 85 86 private final SupportedWindowFeatures mSupportedWindowFeatures; 87 WindowLayoutComponentImpl(@onNull Context context, @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer)88 public WindowLayoutComponentImpl(@NonNull Context context, 89 @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) { 90 ((Application) context.getApplicationContext()) 91 .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); 92 mFoldingFeatureProducer = foldingFeatureProducer; 93 mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); 94 final List<DisplayFoldFeature> displayFoldFeatures = 95 DisplayFoldFeatureUtil.extractDisplayFoldFeatures(mFoldingFeatureProducer); 96 mSupportedWindowFeatures = new SupportedWindowFeatures.Builder(displayFoldFeatures).build(); 97 } 98 99 /** 100 * Adds a listener interested in receiving updates to {@link WindowLayoutInfo} 101 * 102 * @param activity hosting a {@link android.view.Window} 103 * @param consumer interested in receiving updates to {@link WindowLayoutInfo} 104 */ 105 @Override addWindowLayoutInfoListener(@onNull Activity activity, @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer)106 public void addWindowLayoutInfoListener(@NonNull Activity activity, 107 @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { 108 final Consumer<WindowLayoutInfo> extConsumer = consumer::accept; 109 synchronized (mLock) { 110 mJavaToExtConsumers.put(consumer, extConsumer); 111 updateListenerRegistrations(); 112 } 113 addWindowLayoutInfoListener(activity, extConsumer); 114 } 115 116 /** 117 * Similar to {@link #addWindowLayoutInfoListener(Activity, java.util.function.Consumer)}, but 118 * takes a UI Context as a parameter. 119 * 120 * Jetpack {@link androidx.window.layout.ExtensionWindowLayoutInfoBackend} makes sure all 121 * consumers related to the same {@link Context} gets updated {@link WindowLayoutInfo} 122 * together. However only the first registered consumer of a {@link Context} will actually 123 * invoke {@link #addWindowLayoutInfoListener(Context, Consumer)}. 124 * Here we enforce that {@link #addWindowLayoutInfoListener(Context, Consumer)} can only be 125 * called once for each {@link Context}. 126 */ 127 @Override addWindowLayoutInfoListener(@onNull @iContext Context context, @NonNull Consumer<WindowLayoutInfo> consumer)128 public void addWindowLayoutInfoListener(@NonNull @UiContext Context context, 129 @NonNull Consumer<WindowLayoutInfo> consumer) { 130 synchronized (mLock) { 131 if (mWindowLayoutChangeListeners.containsKey(context) 132 // In theory this method can be called on the same consumer with different 133 // context. 134 || containsConsumer(consumer)) { 135 return; 136 } 137 if (!context.isUiContext()) { 138 throw new IllegalArgumentException("Context must be a UI Context, which should be" 139 + " an Activity, WindowContext or InputMethodService"); 140 } 141 mFoldingFeatureProducer.getData((features) -> { 142 WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features); 143 consumer.accept(newWindowLayout); 144 }); 145 mWindowLayoutChangeListeners.put(context, new DeduplicateConsumer<>(consumer)); 146 147 final IBinder windowContextToken = context.getWindowContextToken(); 148 if (windowContextToken != null) { 149 // We register component callbacks for window contexts. For activity contexts, they 150 // will receive callbacks from NotifyOnConfigurationChanged instead. 151 final ConfigurationChangeListener listener = 152 new ConfigurationChangeListener(windowContextToken); 153 context.registerComponentCallbacks(listener); 154 mConfigurationChangeListeners.put(windowContextToken, listener); 155 } 156 } 157 } 158 159 @Override removeWindowLayoutInfoListener( @onNull java.util.function.Consumer<WindowLayoutInfo> consumer)160 public void removeWindowLayoutInfoListener( 161 @NonNull java.util.function.Consumer<WindowLayoutInfo> consumer) { 162 final Consumer<WindowLayoutInfo> extConsumer; 163 synchronized (mLock) { 164 extConsumer = mJavaToExtConsumers.remove(consumer); 165 updateListenerRegistrations(); 166 } 167 if (extConsumer != null) { 168 removeWindowLayoutInfoListener(extConsumer); 169 } 170 } 171 172 /** 173 * Removes a listener no longer interested in receiving updates. 174 * 175 * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} 176 */ 177 @Override removeWindowLayoutInfoListener(@onNull Consumer<WindowLayoutInfo> consumer)178 public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) { 179 synchronized (mLock) { 180 DeduplicateConsumer<WindowLayoutInfo> consumerToRemove = null; 181 for (Context context : mWindowLayoutChangeListeners.keySet()) { 182 final DeduplicateConsumer<WindowLayoutInfo> deduplicateConsumer = 183 mWindowLayoutChangeListeners.get(context); 184 if (!deduplicateConsumer.matchesConsumer(consumer)) { 185 continue; 186 } 187 final IBinder token = context.getWindowContextToken(); 188 consumerToRemove = deduplicateConsumer; 189 if (token != null) { 190 context.unregisterComponentCallbacks(mConfigurationChangeListeners.get(token)); 191 mConfigurationChangeListeners.remove(token); 192 } 193 break; 194 } 195 if (consumerToRemove != null) { 196 mWindowLayoutChangeListeners.values().remove(consumerToRemove); 197 } 198 } 199 } 200 201 @GuardedBy("mLock") containsConsumer(@onNull Consumer<WindowLayoutInfo> consumer)202 private boolean containsConsumer(@NonNull Consumer<WindowLayoutInfo> consumer) { 203 for (DeduplicateConsumer<WindowLayoutInfo> c : mWindowLayoutChangeListeners.values()) { 204 if (c.matchesConsumer(consumer)) { 205 return true; 206 } 207 } 208 return false; 209 } 210 211 @GuardedBy("mLock") updateListenerRegistrations()212 private void updateListenerRegistrations() { 213 ActivityThread currentThread = ActivityThread.currentActivityThread(); 214 if (mJavaToExtConsumers.isEmpty()) { 215 currentThread.removeConfigurationChangedListener(mRawConfigurationChangedListener); 216 } else { 217 currentThread.addConfigurationChangedListener(Runnable::run, 218 mRawConfigurationChangedListener); 219 } 220 } 221 222 @GuardedBy("mLock") 223 @NonNull getContextsListeningForLayoutChanges()224 private Set<Context> getContextsListeningForLayoutChanges() { 225 return mWindowLayoutChangeListeners.keySet(); 226 } 227 228 @GuardedBy("mLock") isListeningForLayoutChanges(IBinder token)229 private boolean isListeningForLayoutChanges(IBinder token) { 230 for (Context context : getContextsListeningForLayoutChanges()) { 231 if (token.equals(Context.getToken(context))) { 232 return true; 233 } 234 } 235 return false; 236 } 237 238 /** 239 * A convenience method to translate from the common feature state to the extensions feature 240 * state. More specifically, translates from {@link CommonFoldingFeature.State} to 241 * {@link FoldingFeature#STATE_FLAT} or {@link FoldingFeature#STATE_HALF_OPENED}. If it is not 242 * possible to translate, then we will return a {@code null} value. 243 * 244 * @param state if it matches a value in {@link CommonFoldingFeature.State}, {@code null} 245 * otherwise. @return a {@link FoldingFeature#STATE_FLAT} or 246 * {@link FoldingFeature#STATE_HALF_OPENED} if the given state matches a value in 247 * {@link CommonFoldingFeature.State} and {@code null} otherwise. 248 */ 249 @Nullable convertToExtensionState(int state)250 private Integer convertToExtensionState(int state) { 251 if (state == COMMON_STATE_FLAT) { 252 return FoldingFeature.STATE_FLAT; 253 } else if (state == COMMON_STATE_HALF_OPENED) { 254 return FoldingFeature.STATE_HALF_OPENED; 255 } else { 256 return null; 257 } 258 } 259 onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures)260 private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) { 261 synchronized (mLock) { 262 mLastReportedFoldingFeatures.clear(); 263 mLastReportedFoldingFeatures.addAll(storedFeatures); 264 for (Context context : getContextsListeningForLayoutChanges()) { 265 // Get the WindowLayoutInfo from the activity and pass the value to the 266 // layoutConsumer. 267 Consumer<WindowLayoutInfo> layoutConsumer = mWindowLayoutChangeListeners.get( 268 context); 269 WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, storedFeatures); 270 layoutConsumer.accept(newWindowLayout); 271 } 272 } 273 } 274 275 /** 276 * Translates the {@link DisplayFeature} into a {@link WindowLayoutInfo} when a 277 * valid state is found. 278 * 279 * @param context a proxy for the {@link android.view.Window} that contains the 280 * {@link DisplayFeature}. 281 */ getWindowLayoutInfo(@onNull @iContext Context context, List<CommonFoldingFeature> storedFeatures)282 private WindowLayoutInfo getWindowLayoutInfo(@NonNull @UiContext Context context, 283 List<CommonFoldingFeature> storedFeatures) { 284 List<DisplayFeature> displayFeatureList = getDisplayFeatures(context, storedFeatures); 285 return new WindowLayoutInfo(displayFeatureList); 286 } 287 288 /** 289 * Gets the current {@link WindowLayoutInfo} computed with passed {@link WindowConfiguration}. 290 * 291 * @return current {@link WindowLayoutInfo} on the default display. Returns 292 * empty {@link WindowLayoutInfo} on secondary displays. 293 */ 294 @NonNull getCurrentWindowLayoutInfo(int displayId, @NonNull WindowConfiguration windowConfiguration)295 public WindowLayoutInfo getCurrentWindowLayoutInfo(int displayId, 296 @NonNull WindowConfiguration windowConfiguration) { 297 synchronized (mLock) { 298 return getWindowLayoutInfo(displayId, windowConfiguration, 299 mLastReportedFoldingFeatures); 300 } 301 } 302 303 /** 304 * Returns the {@link SupportedWindowFeatures} for the device. This list does not change over 305 * time. 306 */ 307 @NonNull getSupportedWindowFeatures()308 public SupportedWindowFeatures getSupportedWindowFeatures() { 309 return mSupportedWindowFeatures; 310 } 311 312 /** @see #getWindowLayoutInfo(Context, List) */ getWindowLayoutInfo(int displayId, @NonNull WindowConfiguration windowConfiguration, List<CommonFoldingFeature> storedFeatures)313 private WindowLayoutInfo getWindowLayoutInfo(int displayId, 314 @NonNull WindowConfiguration windowConfiguration, 315 List<CommonFoldingFeature> storedFeatures) { 316 List<DisplayFeature> displayFeatureList = getDisplayFeatures(displayId, windowConfiguration, 317 storedFeatures); 318 return new WindowLayoutInfo(displayFeatureList); 319 } 320 321 /** 322 * Translate from the {@link CommonFoldingFeature} to 323 * {@link DisplayFeature} for a given {@link Activity}. If a 324 * {@link CommonFoldingFeature} is not valid then it will be omitted. 325 * 326 * For a {@link FoldingFeature} the bounds are localized into the {@link Activity} window 327 * coordinate space and the state is calculated from {@link CommonFoldingFeature#getState()}. 328 * The state from {@link #mFoldingFeatureProducer} may not be valid since 329 * {@link #mFoldingFeatureProducer} is a general state controller. If the state is not valid, 330 * the {@link FoldingFeature} is omitted from the {@link List} of {@link DisplayFeature}. If the 331 * bounds are not valid, constructing a {@link FoldingFeature} will throw an 332 * {@link IllegalArgumentException} since this can cause negative UI effects down stream. 333 * 334 * @param context a proxy for the {@link android.view.Window} that contains the 335 * {@link DisplayFeature}. 336 * @return a {@link List} of {@link DisplayFeature}s that are within the 337 * {@link android.view.Window} of the {@link Activity} 338 */ getDisplayFeatures( @onNull @iContext Context context, List<CommonFoldingFeature> storedFeatures)339 private List<DisplayFeature> getDisplayFeatures( 340 @NonNull @UiContext Context context, List<CommonFoldingFeature> storedFeatures) { 341 if (!shouldReportDisplayFeatures(context)) { 342 return Collections.emptyList(); 343 } 344 return getDisplayFeatures(context.getDisplayId(), 345 context.getResources().getConfiguration().windowConfiguration, 346 storedFeatures); 347 } 348 349 /** @see #getDisplayFeatures(Context, List) */ getDisplayFeatures(int displayId, @NonNull WindowConfiguration windowConfiguration, List<CommonFoldingFeature> storedFeatures)350 private List<DisplayFeature> getDisplayFeatures(int displayId, 351 @NonNull WindowConfiguration windowConfiguration, 352 List<CommonFoldingFeature> storedFeatures) { 353 List<DisplayFeature> features = new ArrayList<>(); 354 if (displayId != DEFAULT_DISPLAY) { 355 return features; 356 } 357 358 // We will transform the feature bounds to the Activity window, so using the rotation 359 // from the same source (WindowConfiguration) to make sure they are synchronized. 360 final int rotation = windowConfiguration.getDisplayRotation(); 361 362 for (CommonFoldingFeature baseFeature : storedFeatures) { 363 Integer state = convertToExtensionState(baseFeature.getState()); 364 if (state == null) { 365 continue; 366 } 367 Rect featureRect = baseFeature.getRect(); 368 rotateRectToDisplayRotation(displayId, rotation, featureRect); 369 transformToWindowSpaceRect(windowConfiguration, featureRect); 370 371 if (isZero(featureRect)) { 372 // TODO(b/228641877): Remove guarding when fixed. 373 continue; 374 } 375 if (featureRect.left != 0 && featureRect.top != 0) { 376 Log.wtf(TAG, "Bounding rectangle must start at the top or " 377 + "left of the window. BaseFeatureRect: " + baseFeature.getRect() 378 + ", FeatureRect: " + featureRect 379 + ", WindowConfiguration: " + windowConfiguration); 380 continue; 381 382 } 383 if (featureRect.left == 0 384 && featureRect.width() != windowConfiguration.getBounds().width()) { 385 Log.w(TAG, "Horizontal FoldingFeature must have full width." 386 + " BaseFeatureRect: " + baseFeature.getRect() 387 + ", FeatureRect: " + featureRect 388 + ", WindowConfiguration: " + windowConfiguration); 389 continue; 390 } 391 if (featureRect.top == 0 392 && featureRect.height() != windowConfiguration.getBounds().height()) { 393 Log.w(TAG, "Vertical FoldingFeature must have full height." 394 + " BaseFeatureRect: " + baseFeature.getRect() 395 + ", FeatureRect: " + featureRect 396 + ", WindowConfiguration: " + windowConfiguration); 397 continue; 398 } 399 features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); 400 } 401 return features; 402 } 403 404 /** 405 * Calculates if the display features should be reported for the UI Context. The calculation 406 * uses the task information because that is accurate for Activities in ActivityEmbedding mode. 407 * TODO(b/238948678): Support reporting display features in all windowing modes. 408 * 409 * @return true if the display features should be reported for the UI Context, false otherwise. 410 */ shouldReportDisplayFeatures(@onNull @iContext Context context)411 private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) { 412 int displayId = context.getDisplay().getDisplayId(); 413 if (displayId != DEFAULT_DISPLAY) { 414 // Display features are not supported on secondary displays. 415 return false; 416 } 417 418 // We do not report folding features for Activities in PiP because the bounds are 419 // not updated fast enough and the window is too small for the UI to adapt. 420 return context.getResources().getConfiguration().windowConfiguration 421 .getWindowingMode() != WindowConfiguration.WINDOWING_MODE_PINNED; 422 } 423 424 @GuardedBy("mLock") onDisplayFeaturesChangedIfListening(@onNull IBinder token)425 private void onDisplayFeaturesChangedIfListening(@NonNull IBinder token) { 426 if (isListeningForLayoutChanges(token)) { 427 mFoldingFeatureProducer.getData( 428 WindowLayoutComponentImpl.this::onDisplayFeaturesChanged); 429 } 430 } 431 432 private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter { 433 @Override onActivityCreated(Activity activity, Bundle savedInstanceState)434 public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 435 super.onActivityCreated(activity, savedInstanceState); 436 synchronized (mLock) { 437 onDisplayFeaturesChangedIfListening(activity.getActivityToken()); 438 } 439 } 440 441 @Override onActivityConfigurationChanged(Activity activity)442 public void onActivityConfigurationChanged(Activity activity) { 443 super.onActivityConfigurationChanged(activity); 444 synchronized (mLock) { 445 onDisplayFeaturesChangedIfListening(activity.getActivityToken()); 446 } 447 } 448 } 449 450 private final class RawConfigurationChangedListener implements 451 java.util.function.Consumer<IBinder> { 452 @Override accept(IBinder activityToken)453 public void accept(IBinder activityToken) { 454 synchronized (mLock) { 455 onDisplayFeaturesChangedIfListening(activityToken); 456 } 457 } 458 } 459 460 private final class ConfigurationChangeListener implements ComponentCallbacks { 461 final IBinder mToken; 462 ConfigurationChangeListener(IBinder token)463 ConfigurationChangeListener(IBinder token) { 464 mToken = token; 465 } 466 467 @Override onConfigurationChanged(@onNull Configuration newConfig)468 public void onConfigurationChanged(@NonNull Configuration newConfig) { 469 synchronized (mLock) { 470 onDisplayFeaturesChangedIfListening(mToken); 471 } 472 } 473 474 @Override onLowMemory()475 public void onLowMemory() { 476 } 477 } 478 } 479