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.DisplayFeature.COMMON_STATE_FLAT; 22 import static androidx.window.common.DisplayFeature.COMMON_STATE_HALF_OPENED; 23 import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; 24 import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; 25 26 import android.annotation.Nullable; 27 import android.app.Activity; 28 import android.content.Context; 29 import android.graphics.Rect; 30 import android.util.Log; 31 32 import androidx.annotation.NonNull; 33 import androidx.window.common.DeviceStateManagerPostureProducer; 34 import androidx.window.common.DisplayFeature; 35 import androidx.window.common.ResourceConfigDisplayFeatureProducer; 36 import androidx.window.common.SettingsDevicePostureProducer; 37 import androidx.window.common.SettingsDisplayFeatureProducer; 38 import androidx.window.util.DataProducer; 39 import androidx.window.util.PriorityDataProducer; 40 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Optional; 46 import java.util.Set; 47 import java.util.function.Consumer; 48 49 /** 50 * Reference implementation of androidx.window.extensions.layout OEM interface for use with 51 * WindowManager Jetpack. 52 * 53 * NOTE: This version is a work in progress and under active development. It MUST NOT be used in 54 * production builds since the interface can still change before reaching stable version. 55 * Please refer to {@link androidx.window.sidecar.SampleSidecarImpl} instead. 56 */ 57 public class WindowLayoutComponentImpl implements WindowLayoutComponent { 58 private static final String TAG = "SampleExtension"; 59 private static WindowLayoutComponent sInstance; 60 61 private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = 62 new HashMap<>(); 63 64 private final SettingsDevicePostureProducer mSettingsDevicePostureProducer; 65 private final DataProducer<Integer> mDevicePostureProducer; 66 67 private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer; 68 private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer; 69 WindowLayoutComponentImpl(Context context)70 public WindowLayoutComponentImpl(Context context) { 71 mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context); 72 mDevicePostureProducer = new PriorityDataProducer<>(List.of( 73 mSettingsDevicePostureProducer, 74 new DeviceStateManagerPostureProducer(context) 75 )); 76 77 mSettingsDisplayFeatureProducer = new SettingsDisplayFeatureProducer(context); 78 mDisplayFeatureProducer = new PriorityDataProducer<>(List.of( 79 mSettingsDisplayFeatureProducer, 80 new ResourceConfigDisplayFeatureProducer(context) 81 )); 82 83 mDevicePostureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); 84 mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); 85 } 86 87 /** 88 * Adds a listener interested in receiving updates to {@link WindowLayoutInfo} 89 * @param activity hosting a {@link android.view.Window} 90 * @param consumer interested in receiving updates to {@link WindowLayoutInfo} 91 */ addWindowLayoutInfoListener(@onNull Activity activity, @NonNull Consumer<WindowLayoutInfo> consumer)92 public void addWindowLayoutInfoListener(@NonNull Activity activity, 93 @NonNull Consumer<WindowLayoutInfo> consumer) { 94 mWindowLayoutChangeListeners.put(activity, consumer); 95 updateRegistrations(); 96 } 97 98 /** 99 * Removes a listener no longer interested in receiving updates. 100 * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} 101 */ removeWindowLayoutInfoListener( @onNull Consumer<WindowLayoutInfo> consumer)102 public void removeWindowLayoutInfoListener( 103 @NonNull Consumer<WindowLayoutInfo> consumer) { 104 mWindowLayoutChangeListeners.values().remove(consumer); 105 updateRegistrations(); 106 } 107 updateWindowLayout(@onNull Activity activity, @NonNull WindowLayoutInfo newLayout)108 void updateWindowLayout(@NonNull Activity activity, 109 @NonNull WindowLayoutInfo newLayout) { 110 Consumer<WindowLayoutInfo> consumer = mWindowLayoutChangeListeners.get(activity); 111 if (consumer != null) { 112 consumer.accept(newLayout); 113 } 114 } 115 116 @NonNull getActivitiesListeningForLayoutChanges()117 Set<Activity> getActivitiesListeningForLayoutChanges() { 118 return mWindowLayoutChangeListeners.keySet(); 119 } 120 hasListeners()121 protected boolean hasListeners() { 122 return !mWindowLayoutChangeListeners.isEmpty(); 123 } 124 125 /** 126 * Calculate the {@link DisplayFeature.State} from the feature or the device posture producer. 127 * If the given {@link DisplayFeature.State} is not valid then {@code null} will be returned. 128 * The {@link FoldingFeature} should be ignored in the case of an invalid 129 * {@link DisplayFeature.State}. 130 * 131 * @param feature a {@link DisplayFeature} to provide the feature state if present. 132 * @return {@link DisplayFeature.State} of the hinge if present or the state from the posture 133 * produce if present. 134 */ 135 @Nullable getFeatureState(DisplayFeature feature)136 private Integer getFeatureState(DisplayFeature feature) { 137 Integer featureState = feature.getState(); 138 Optional<Integer> posture = mDevicePostureProducer.getData(); 139 Integer state = featureState == null ? posture.orElse(null) : featureState; 140 return convertToExtensionState(state); 141 } 142 143 /** 144 * A convenience method to translate from the common feature state to the extensions feature 145 * state. More specifically, translates from {@link DisplayFeature.State} to 146 * {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED}. If it is not 147 * possible to translate, then we will return a {@code null} value. 148 * 149 * @param state if it matches a value in {@link DisplayFeature.State}, {@code null} otherwise. 150 * @return a {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED} if 151 * the given state matches a value in {@link DisplayFeature.State} and {@code null} otherwise. 152 */ 153 @Nullable convertToExtensionState(@ullable Integer state)154 private Integer convertToExtensionState(@Nullable Integer state) { 155 if (state == null) { // The null check avoids a NullPointerException. 156 return null; 157 } else if (state == COMMON_STATE_FLAT) { 158 return FoldingFeature.STATE_FLAT; 159 } else if (state == COMMON_STATE_HALF_OPENED) { 160 return FoldingFeature.STATE_HALF_OPENED; 161 } else { 162 return null; 163 } 164 } 165 onDisplayFeaturesChanged()166 private void onDisplayFeaturesChanged() { 167 for (Activity activity : getActivitiesListeningForLayoutChanges()) { 168 WindowLayoutInfo newLayout = getWindowLayoutInfo(activity); 169 updateWindowLayout(activity, newLayout); 170 } 171 } 172 173 @NonNull getWindowLayoutInfo(@onNull Activity activity)174 private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { 175 List<androidx.window.extensions.layout.DisplayFeature> displayFeatures = 176 getDisplayFeatures(activity); 177 return new WindowLayoutInfo(displayFeatures); 178 } 179 180 /** 181 * Translate from the {@link DisplayFeature} to 182 * {@link androidx.window.extensions.layout.DisplayFeature} for a given {@link Activity}. If a 183 * {@link DisplayFeature} is not valid then it will be omitted. 184 * 185 * For a {@link FoldingFeature} the bounds are localized into the {@link Activity} window 186 * coordinate space and the state is calculated either from {@link DisplayFeature#getState()} or 187 * {@link #mDisplayFeatureProducer}. The state from {@link #mDisplayFeatureProducer} may not be 188 * valid since {@link #mDisplayFeatureProducer} is a general state controller. If the state is 189 * not valid, the {@link FoldingFeature} is omitted from the {@link List} of 190 * {@link androidx.window.extensions.layout.DisplayFeature}. If the bounds are not valid, 191 * constructing a {@link FoldingFeature} will throw an {@link IllegalArgumentException} since 192 * this can cause negative UI effects down stream. 193 * 194 * @param activity a proxy for the {@link android.view.Window} that contains the 195 * {@link androidx.window.extensions.layout.DisplayFeature}. 196 * @return a {@link List} of valid {@link androidx.window.extensions.layout.DisplayFeature} that 197 * are within the {@link android.view.Window} of the {@link Activity} 198 */ getDisplayFeatures( @onNull Activity activity)199 private List<androidx.window.extensions.layout.DisplayFeature> getDisplayFeatures( 200 @NonNull Activity activity) { 201 List<androidx.window.extensions.layout.DisplayFeature> features = new ArrayList<>(); 202 int displayId = activity.getDisplay().getDisplayId(); 203 if (displayId != DEFAULT_DISPLAY) { 204 Log.w(TAG, "This sample doesn't support display features on secondary displays"); 205 return features; 206 } 207 208 if (activity.isInMultiWindowMode()) { 209 // It is recommended not to report any display features in multi-window mode, since it 210 // won't be possible to synchronize the display feature positions with window movement. 211 return features; 212 } 213 214 Optional<List<DisplayFeature>> storedFeatures = mDisplayFeatureProducer.getData(); 215 if (storedFeatures.isPresent()) { 216 217 for (DisplayFeature baseFeature : storedFeatures.get()) { 218 Integer state = getFeatureState(baseFeature); 219 if (state == null) { 220 continue; 221 } 222 Rect featureRect = baseFeature.getRect(); 223 rotateRectToDisplayRotation(displayId, featureRect); 224 transformToWindowSpaceRect(activity, featureRect); 225 226 features.add(new FoldingFeature(featureRect, baseFeature.getType(), 227 getFeatureState(baseFeature))); 228 } 229 } 230 return features; 231 } 232 updateRegistrations()233 private void updateRegistrations() { 234 if (hasListeners()) { 235 mSettingsDevicePostureProducer.registerObserversIfNeeded(); 236 mSettingsDisplayFeatureProducer.registerObserversIfNeeded(); 237 } else { 238 mSettingsDevicePostureProducer.unregisterObserversIfNeeded(); 239 mSettingsDisplayFeatureProducer.unregisterObserversIfNeeded(); 240 } 241 242 onDisplayFeaturesChanged(); 243 } 244 } 245