1 /* 2 * Copyright (C) 2024 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 android.service.dreams.cts; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import android.annotation.IntDef; 22 import android.app.dream.cts.app.IControlledDream; 23 import android.app.dream.cts.app.IDreamLifecycleListener; 24 import android.app.dream.cts.app.IDreamListener; 25 import android.app.dream.cts.app.IDreamProxy; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.ServiceConnection; 30 import android.os.IBinder; 31 import android.os.RemoteException; 32 import android.server.wm.DreamCoordinator; 33 import android.util.ArraySet; 34 import android.util.Log; 35 36 import java.lang.annotation.Retention; 37 import java.lang.annotation.RetentionPolicy; 38 import java.util.ArrayList; 39 import java.util.Set; 40 import java.util.concurrent.CountDownLatch; 41 import java.util.concurrent.TimeUnit; 42 import java.util.function.Consumer; 43 44 /** 45 * {@link ControlledDreamSession} manages connecting and accessing a controlled dream instance that 46 * is published through a dream proxy. 47 */ 48 public class ControlledDreamSession { 49 private static final String TAG = "ControlledDreamSession"; 50 51 // Timeout that is used for waiting on various steps to complete, such as connecting to the 52 // proxy service and starting the dream. 53 private static final int TIMEOUT_SECONDS = 2; 54 55 // The test app's proxy service component. 56 private static final String DREAM_CONTROL_COMPONENT = 57 "android.app.dream.cts.app/.DreamProxyService"; 58 59 60 public static final int DREAM_LIFECYCLE_UNKNOWN = 0; 61 public static final int DREAM_LIFECYCLE_ON_ATTACHED_TO_WINDOW = 1; 62 public static final int DREAM_LIFECYCLE_ON_DREAMING_STARTED = 2; 63 public static final int DREAM_LIFECYCLE_ON_FOCUS_GAINED = 3; 64 public static final int DREAM_LIFECYCLE_ON_WAKEUP = 4; 65 public static final int DREAM_LIFECYCLE_ON_DREAMING_STOPPED = 5; 66 public static final int DREAM_LIFECYCLE_ON_DETACHED_FROM_WINDOW = 6; 67 public static final int DREAM_LIFECYCLE_ON_DESTROYED = 7; 68 69 @IntDef(prefix = { "DREAM_LIFECYCLE_" }, value = { 70 DREAM_LIFECYCLE_UNKNOWN, 71 DREAM_LIFECYCLE_ON_ATTACHED_TO_WINDOW, 72 DREAM_LIFECYCLE_ON_DREAMING_STARTED, 73 DREAM_LIFECYCLE_ON_FOCUS_GAINED, 74 DREAM_LIFECYCLE_ON_WAKEUP, 75 DREAM_LIFECYCLE_ON_DREAMING_STOPPED, 76 DREAM_LIFECYCLE_ON_DETACHED_FROM_WINDOW, 77 DREAM_LIFECYCLE_ON_DESTROYED, 78 }) 79 @Retention(RetentionPolicy.SOURCE) 80 public @interface Dreamlifecycle{} 81 82 /** 83 * Returns a string description for the lifecycle. 84 */ lifecycleToString(@reamlifecycle int lifecycle)85 public static String lifecycleToString(@Dreamlifecycle int lifecycle) { 86 return switch (lifecycle) { 87 case DREAM_LIFECYCLE_UNKNOWN -> "unknown"; 88 case DREAM_LIFECYCLE_ON_ATTACHED_TO_WINDOW -> "attached_to_window"; 89 case DREAM_LIFECYCLE_ON_DREAMING_STARTED -> "on_dream_started"; 90 case DREAM_LIFECYCLE_ON_FOCUS_GAINED -> "on_focus_gained"; 91 case DREAM_LIFECYCLE_ON_WAKEUP -> "on_wake_up"; 92 case DREAM_LIFECYCLE_ON_DREAMING_STOPPED -> "on_dreaming_stopped"; 93 case DREAM_LIFECYCLE_ON_DETACHED_FROM_WINDOW -> "on_detached_from_window"; 94 case DREAM_LIFECYCLE_ON_DESTROYED -> "on_destroyed"; 95 default -> "not found"; 96 }; 97 } 98 99 100 // Connection for accessing the dream proxy. 101 private static final class ProxyServiceConnection implements ServiceConnection { 102 private final CountDownLatch mLatch; 103 private final IBinder.DeathRecipient mDeathRecipient; 104 private IDreamProxy mProxy; 105 ProxyServiceConnection(CountDownLatch latch, IBinder.DeathRecipient deathRecipient)106 ProxyServiceConnection(CountDownLatch latch, IBinder.DeathRecipient deathRecipient) { 107 mLatch = latch; 108 mDeathRecipient = deathRecipient; 109 } 110 getProxy()111 public IDreamProxy getProxy() { 112 return mProxy; 113 } 114 115 @Override onServiceConnected(ComponentName name, IBinder service)116 public void onServiceConnected(ComponentName name, IBinder service) { 117 mProxy = IDreamProxy.Stub.asInterface(service); 118 try { 119 service.linkToDeath(mDeathRecipient, 0); 120 } catch (RemoteException e) { 121 Log.e(TAG, "could not link to death", e); 122 } 123 mLatch.countDown(); 124 } 125 126 @Override onServiceDisconnected(ComponentName name)127 public void onServiceDisconnected(ComponentName name) { 128 mProxy = null; 129 } 130 } 131 132 private final Context mContext; 133 private final ComponentName mDreamComponent; 134 private final DreamCoordinator mDreamCoordinator; 135 136 private ProxyServiceConnection mServiceConnection; 137 138 private IControlledDream mControlledDream; 139 140 private ArrayList<Integer> mSeenLifecycles = new ArrayList<>(); 141 142 private final Set<Consumer<Integer>> mLifecycleConsumers = new ArraySet<>(); 143 ControlledDreamSession(Context context, ComponentName dreamComponent, DreamCoordinator coordinator)144 public ControlledDreamSession(Context context, ComponentName dreamComponent, 145 DreamCoordinator coordinator) { 146 mContext = context; 147 mDreamComponent = dreamComponent; 148 mDreamCoordinator = coordinator; 149 } 150 151 private IDreamLifecycleListener mLifecycleListener = new IDreamLifecycleListener.Stub() { 152 public void onAttachedToWindow(IControlledDream dream) { 153 pushLifecycle(DREAM_LIFECYCLE_ON_ATTACHED_TO_WINDOW); 154 } 155 156 public void onDreamingStarted(IControlledDream dream) { 157 pushLifecycle(DREAM_LIFECYCLE_ON_DREAMING_STARTED); 158 } 159 160 public void onFocusChanged(IControlledDream dream, boolean hasFocus) { 161 if (hasFocus) { 162 pushLifecycle(DREAM_LIFECYCLE_ON_FOCUS_GAINED); 163 } 164 } 165 166 public void onDreamingStopped(IControlledDream dream) { 167 pushLifecycle(DREAM_LIFECYCLE_ON_DREAMING_STOPPED); 168 } 169 170 public void onWakeUp(IControlledDream dream) { 171 pushLifecycle(DREAM_LIFECYCLE_ON_WAKEUP); 172 } 173 174 public void onDetachedFromWindow(IControlledDream dream) { 175 pushLifecycle(DREAM_LIFECYCLE_ON_DETACHED_FROM_WINDOW); 176 } 177 178 public void onDreamDestroyed(IControlledDream dream) { 179 pushLifecycle(DREAM_LIFECYCLE_ON_DESTROYED); 180 } 181 }; 182 pushLifecycle(@reamlifecycle int lifecycle)183 private void pushLifecycle(@Dreamlifecycle int lifecycle) { 184 mSeenLifecycles.add(lifecycle); 185 // Make a copy of the set to prevent concurrent modification. 186 final Set<Consumer<Integer>> consumers = new ArraySet<>(); 187 consumers.addAll(mLifecycleConsumers); 188 consumers.forEach(consumer -> consumer.accept(lifecycle)); 189 } 190 191 /** 192 * Sets the dream component specified at construction as the active dream and subsequently 193 * starts said dream. 194 * @return An {@link IControlledDream} for accessing the currently running dream. 195 */ start()196 public IControlledDream start() throws InterruptedException, RemoteException { 197 if (mServiceConnection != null) { 198 Log.e(TAG, "session already started"); 199 return null; 200 } 201 202 // Connect to dream controller 203 final ComponentName controllerService = 204 ComponentName.unflattenFromString(DREAM_CONTROL_COMPONENT); 205 final Intent intent = new Intent(); 206 intent.setComponent(controllerService); 207 final CountDownLatch countDownLatch = new CountDownLatch(1); 208 mServiceConnection = new ProxyServiceConnection(countDownLatch, 209 new IBinder.DeathRecipient() { 210 @Override 211 public void binderDied() { 212 cleanup(true); 213 } 214 }); 215 mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 216 assertThat(countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue(); 217 218 final CountDownLatch dreamConnectLatch = new CountDownLatch(1); 219 final IDreamListener dreamConnectListener = new IDreamListener.Stub() { 220 @Override 221 public void onDreamPublished(IControlledDream dream) { 222 mControlledDream = dream; 223 dreamConnectLatch.countDown(); 224 } 225 }; 226 227 mServiceConnection.getProxy().registerListener(dreamConnectListener); 228 229 // Start Dream 230 mDreamCoordinator.setActiveDream(mDreamComponent); 231 mDreamCoordinator.startDream(); 232 233 // Wait for dream to connect to the DreamController 234 assertThat(dreamConnectLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue(); 235 mControlledDream.registerLifecycleListener(mLifecycleListener); 236 mServiceConnection.getProxy().unregisterListener(dreamConnectListener); 237 238 return mControlledDream; 239 } 240 241 242 /** 243 * Returns the dream published during start. 244 */ getControlledDream()245 public IControlledDream getControlledDream() { 246 return mControlledDream; 247 } 248 249 /** 250 * Stops the current dream. 251 */ stop()252 public void stop() { 253 cleanup(false); 254 } 255 cleanup(boolean dead)256 private void cleanup(boolean dead) { 257 if (mServiceConnection == null) { 258 Log.e(TAG, "session not started"); 259 return; 260 } 261 262 if (!dead && mControlledDream != null) { 263 try { 264 mControlledDream.unregisterLifecycleListener(mLifecycleListener); 265 } catch (RemoteException e) { 266 Log.e(TAG, "could not unregister lifecycle listener", e); 267 } 268 } 269 270 mControlledDream = null; 271 272 mContext.unbindService(mServiceConnection); 273 mServiceConnection = null; 274 } 275 276 /** 277 * Waits for a lifecycle to be reached, timing out if never reached. 278 */ awaitLifecycle(@reamlifecycle int targetLifecycle)279 public void awaitLifecycle(@Dreamlifecycle int targetLifecycle) throws InterruptedException { 280 final CountDownLatch latch = new CountDownLatch(1); 281 final Consumer<Integer> consumer = lifecycle -> { 282 if (lifecycle == targetLifecycle) { 283 latch.countDown(); 284 } 285 }; 286 287 try { 288 mLifecycleConsumers.add(consumer); 289 290 if (mSeenLifecycles.contains(targetLifecycle)) { 291 return; 292 } 293 294 assertThat(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue(); 295 } finally { 296 mLifecycleConsumers.remove(consumer); 297 } 298 } 299 } 300