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