1 /* 2 * Copyright (C) 2022 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.area; 18 19 import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; 20 21 import android.app.Activity; 22 import android.content.Context; 23 import android.hardware.devicestate.DeviceStateManager; 24 import android.hardware.devicestate.DeviceStateRequest; 25 import android.util.ArraySet; 26 27 import androidx.annotation.NonNull; 28 import androidx.window.extensions.core.util.function.Consumer; 29 30 import com.android.internal.R; 31 import com.android.internal.annotations.GuardedBy; 32 33 import java.util.concurrent.Executor; 34 35 /** 36 * Reference implementation of androidx.window.extensions.area OEM interface for use with 37 * WindowManager Jetpack. 38 * 39 * This component currently supports Rear Display mode with the ability to add and remove 40 * status listeners for this mode. 41 * 42 * The public methods in this class are thread-safe. 43 **/ 44 public class WindowAreaComponentImpl implements WindowAreaComponent, 45 DeviceStateManager.DeviceStateCallback { 46 47 private final Object mLock = new Object(); 48 49 private final DeviceStateManager mDeviceStateManager; 50 private final Executor mExecutor; 51 52 @GuardedBy("mLock") 53 private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>(); 54 private final int mRearDisplayState; 55 @WindowAreaSessionState 56 private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE; 57 58 @GuardedBy("mLock") 59 private int mCurrentDeviceState = INVALID_DEVICE_STATE; 60 @GuardedBy("mLock") 61 private int mCurrentDeviceBaseState = INVALID_DEVICE_STATE; 62 @GuardedBy("mLock") 63 private DeviceStateRequest mDeviceStateRequest; 64 WindowAreaComponentImpl(@onNull Context context)65 public WindowAreaComponentImpl(@NonNull Context context) { 66 mDeviceStateManager = context.getSystemService(DeviceStateManager.class); 67 mExecutor = context.getMainExecutor(); 68 69 // TODO(b/236022708) Move rear display state to device state config file 70 mRearDisplayState = context.getResources().getInteger( 71 R.integer.config_deviceStateRearDisplay); 72 73 mDeviceStateManager.registerCallback(mExecutor, this); 74 } 75 76 /** 77 * Adds a listener interested in receiving updates on the RearDisplayStatus 78 * of the device. Because this is being called from the OEM provided 79 * extensions, we will post the result of the listener on the executor 80 * provided by the developer at the initial call site. 81 * 82 * Depending on the initial state of the device, we will return either 83 * {@link WindowAreaComponent#STATUS_AVAILABLE} or 84 * {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that 85 * state respectively. When the rear display feature is triggered, we update the status to be 86 * {@link WindowAreaComponent#STATUS_UNAVAILABLE}. TODO(b/240727590) Prefix with AREA_ 87 * 88 * TODO(b/239833099) Add a STATUS_ACTIVE option to let apps know if a feature is currently 89 * enabled. 90 * 91 * @param consumer {@link Consumer} interested in receiving updates to the status of 92 * rear display mode. 93 */ addRearDisplayStatusListener( @onNull Consumer<@WindowAreaStatus Integer> consumer)94 public void addRearDisplayStatusListener( 95 @NonNull Consumer<@WindowAreaStatus Integer> consumer) { 96 synchronized (mLock) { 97 mRearDisplayStatusListeners.add(consumer); 98 99 // If current device state is still invalid, we haven't gotten our initial value yet 100 if (mCurrentDeviceState == INVALID_DEVICE_STATE) { 101 return; 102 } 103 consumer.accept(getCurrentStatus()); 104 } 105 } 106 107 /** 108 * Removes a listener no longer interested in receiving updates. 109 * @param consumer no longer interested in receiving updates to RearDisplayStatus 110 */ removeRearDisplayStatusListener( @onNull Consumer<@WindowAreaStatus Integer> consumer)111 public void removeRearDisplayStatusListener( 112 @NonNull Consumer<@WindowAreaStatus Integer> consumer) { 113 synchronized (mLock) { 114 mRearDisplayStatusListeners.remove(consumer); 115 } 116 } 117 118 /** 119 * Creates and starts a rear display session and provides updates to the 120 * callback provided. Because this is being called from the OEM provided 121 * extensions, we will post the result of the listener on the executor 122 * provided by the developer at the initial call site. 123 * 124 * When we enable rear display mode, we submit a request to {@link DeviceStateManager} 125 * to override the device state to the state that corresponds to RearDisplay 126 * mode. When the {@link DeviceStateRequest} is activated, we let the 127 * consumer know that the session is active by sending 128 * {@link WindowAreaComponent#SESSION_STATE_ACTIVE}. 129 * 130 * @param activity to provide updates to the client on 131 * the status of the Session 132 * @param rearDisplaySessionCallback to provide updates to the client on 133 * the status of the Session 134 */ startRearDisplaySession(@onNull Activity activity, @NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback)135 public void startRearDisplaySession(@NonNull Activity activity, 136 @NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) { 137 synchronized (mLock) { 138 if (mDeviceStateRequest != null) { 139 // Rear display session is already active 140 throw new IllegalStateException( 141 "Unable to start new rear display session as one is already active"); 142 } 143 mDeviceStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build(); 144 mDeviceStateManager.requestState( 145 mDeviceStateRequest, 146 mExecutor, 147 new DeviceStateRequestCallbackAdapter(rearDisplaySessionCallback) 148 ); 149 } 150 } 151 152 /** 153 * Ends the current rear display session and provides updates to the 154 * callback provided. Because this is being called from the OEM provided 155 * extensions, we will post the result of the listener on the executor 156 * provided by the developer. 157 */ endRearDisplaySession()158 public void endRearDisplaySession() { 159 synchronized (mLock) { 160 if (mDeviceStateRequest != null || isRearDisplayActive()) { 161 mDeviceStateRequest = null; 162 mDeviceStateManager.cancelStateRequest(); 163 } else { 164 throw new IllegalStateException( 165 "Unable to cancel a rear display session as there is no active session"); 166 } 167 } 168 } 169 170 @Override onBaseStateChanged(int state)171 public void onBaseStateChanged(int state) { 172 synchronized (mLock) { 173 mCurrentDeviceBaseState = state; 174 if (state == mCurrentDeviceState) { 175 updateStatusConsumers(getCurrentStatus()); 176 } 177 } 178 } 179 180 @Override onStateChanged(int state)181 public void onStateChanged(int state) { 182 synchronized (mLock) { 183 mCurrentDeviceState = state; 184 updateStatusConsumers(getCurrentStatus()); 185 } 186 } 187 188 @GuardedBy("mLock") getCurrentStatus()189 private int getCurrentStatus() { 190 if (mRearDisplayState == INVALID_DEVICE_STATE) { 191 return WindowAreaComponent.STATUS_UNSUPPORTED; 192 } 193 194 if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE 195 || isRearDisplayActive()) { 196 return WindowAreaComponent.STATUS_UNAVAILABLE; 197 } 198 return WindowAreaComponent.STATUS_AVAILABLE; 199 } 200 201 /** 202 * Helper method to determine if a rear display session is currently active by checking 203 * if the current device configuration matches that of rear display. This would be true 204 * if there is a device override currently active (base state != current state) and the current 205 * state is that which corresponds to {@code mRearDisplayState} 206 * @return {@code true} if the device is in rear display mode and {@code false} if not 207 */ 208 @GuardedBy("mLock") isRearDisplayActive()209 private boolean isRearDisplayActive() { 210 return (mCurrentDeviceState != mCurrentDeviceBaseState) && (mCurrentDeviceState 211 == mRearDisplayState); 212 } 213 214 @GuardedBy("mLock") updateStatusConsumers(@indowAreaStatus int windowAreaStatus)215 private void updateStatusConsumers(@WindowAreaStatus int windowAreaStatus) { 216 synchronized (mLock) { 217 for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) { 218 mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus); 219 } 220 } 221 } 222 223 /** 224 * Callback for the {@link DeviceStateRequest} to be notified of when the request has been 225 * activated or cancelled. This callback provides information to the client library 226 * on the status of the RearDisplay session through {@code mRearDisplaySessionCallback} 227 */ 228 private class DeviceStateRequestCallbackAdapter implements DeviceStateRequest.Callback { 229 230 private final Consumer<Integer> mRearDisplaySessionCallback; 231 DeviceStateRequestCallbackAdapter(@onNull Consumer<Integer> callback)232 DeviceStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) { 233 mRearDisplaySessionCallback = callback; 234 } 235 236 @Override onRequestActivated(@onNull DeviceStateRequest request)237 public void onRequestActivated(@NonNull DeviceStateRequest request) { 238 synchronized (mLock) { 239 if (request.equals(mDeviceStateRequest)) { 240 mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE; 241 mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus); 242 updateStatusConsumers(getCurrentStatus()); 243 } 244 } 245 } 246 247 @Override onRequestCanceled(DeviceStateRequest request)248 public void onRequestCanceled(DeviceStateRequest request) { 249 synchronized (mLock) { 250 if (request.equals(mDeviceStateRequest)) { 251 mDeviceStateRequest = null; 252 } 253 mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE; 254 mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus); 255 updateStatusConsumers(getCurrentStatus()); 256 } 257 } 258 } 259 260 @Override addRearDisplayPresentationStatusListener( @onNull Consumer<ExtensionWindowAreaStatus> consumer)261 public void addRearDisplayPresentationStatusListener( 262 @NonNull Consumer<ExtensionWindowAreaStatus> consumer) { 263 throw new UnsupportedOperationException( 264 "addRearDisplayPresentationStatusListener is not supported in API_VERSION=2"); 265 } 266 267 @Override removeRearDisplayPresentationStatusListener( @onNull Consumer<ExtensionWindowAreaStatus> consumer)268 public void removeRearDisplayPresentationStatusListener( 269 @NonNull Consumer<ExtensionWindowAreaStatus> consumer) { 270 throw new UnsupportedOperationException( 271 "removeRearDisplayPresentationStatusListener is not supported in API_VERSION=2"); 272 } 273 274 @Override startRearDisplayPresentationSession(@onNull Activity activity, @NonNull Consumer<@WindowAreaSessionState Integer> consumer)275 public void startRearDisplayPresentationSession(@NonNull Activity activity, 276 @NonNull Consumer<@WindowAreaSessionState Integer> consumer) { 277 throw new UnsupportedOperationException( 278 "startRearDisplayPresentationSession is not supported in API_VERSION=2"); 279 } 280 281 @Override endRearDisplayPresentationSession()282 public void endRearDisplayPresentationSession() { 283 throw new UnsupportedOperationException( 284 "endRearDisplayPresentationSession is not supported in API_VERSION=2"); 285 } 286 287 @Override getRearDisplayPresentation()288 public ExtensionWindowAreaPresentation getRearDisplayPresentation() { 289 throw new UnsupportedOperationException( 290 "getRearDisplayPresentation is not supported in API_VERSION=2"); 291 } 292 } 293