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