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