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