• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_FOCUS_LOST = 4;
65     public static final int DREAM_LIFECYCLE_ON_WAKEUP = 5;
66     public static final int DREAM_LIFECYCLE_ON_DREAMING_STOPPED = 6;
67     public static final int DREAM_LIFECYCLE_ON_DETACHED_FROM_WINDOW = 7;
68     public static final int DREAM_LIFECYCLE_ON_DESTROYED = 8;
69 
70     @IntDef(prefix = { "DREAM_LIFECYCLE_" }, value = {
71             DREAM_LIFECYCLE_UNKNOWN,
72             DREAM_LIFECYCLE_ON_ATTACHED_TO_WINDOW,
73             DREAM_LIFECYCLE_ON_DREAMING_STARTED,
74             DREAM_LIFECYCLE_ON_FOCUS_GAINED,
75             DREAM_LIFECYCLE_ON_WAKEUP,
76             DREAM_LIFECYCLE_ON_DREAMING_STOPPED,
77             DREAM_LIFECYCLE_ON_DETACHED_FROM_WINDOW,
78             DREAM_LIFECYCLE_ON_DESTROYED,
79     })
80     @Retention(RetentionPolicy.SOURCE)
81     public @interface Dreamlifecycle{}
82 
83     /**
84      * Returns a string description for the lifecycle.
85      */
lifecycleToString(@reamlifecycle int lifecycle)86     public static String lifecycleToString(@Dreamlifecycle int lifecycle) {
87         return switch (lifecycle) {
88             case DREAM_LIFECYCLE_UNKNOWN -> "unknown";
89             case DREAM_LIFECYCLE_ON_ATTACHED_TO_WINDOW -> "attached_to_window";
90             case DREAM_LIFECYCLE_ON_DREAMING_STARTED -> "on_dream_started";
91             case DREAM_LIFECYCLE_ON_FOCUS_GAINED -> "on_focus_gained";
92             case DREAM_LIFECYCLE_ON_WAKEUP -> "on_wake_up";
93             case DREAM_LIFECYCLE_ON_DREAMING_STOPPED -> "on_dreaming_stopped";
94             case DREAM_LIFECYCLE_ON_DETACHED_FROM_WINDOW -> "on_detached_from_window";
95             case DREAM_LIFECYCLE_ON_DESTROYED -> "on_destroyed";
96             default -> "not found";
97         };
98     }
99 
100 
101     // Connection for accessing the dream proxy.
102     private static final class ProxyServiceConnection implements ServiceConnection {
103         private final CountDownLatch mLatch;
104         private final IBinder.DeathRecipient mDeathRecipient;
105         private IDreamProxy mProxy;
106 
ProxyServiceConnection(CountDownLatch latch, IBinder.DeathRecipient deathRecipient)107         ProxyServiceConnection(CountDownLatch latch, IBinder.DeathRecipient deathRecipient) {
108             mLatch = latch;
109             mDeathRecipient = deathRecipient;
110         }
111 
getProxy()112         public IDreamProxy getProxy() {
113             return mProxy;
114         }
115 
116         @Override
onServiceConnected(ComponentName name, IBinder service)117         public void onServiceConnected(ComponentName name, IBinder service) {
118             mProxy = IDreamProxy.Stub.asInterface(service);
119             try {
120                 service.linkToDeath(mDeathRecipient, 0);
121             } catch (RemoteException e) {
122                 Log.e(TAG, "could not link to death", e);
123             }
124             mLatch.countDown();
125         }
126 
127         @Override
onServiceDisconnected(ComponentName name)128         public void onServiceDisconnected(ComponentName name) {
129             mProxy = null;
130         }
131     }
132 
133     private final Context mContext;
134     private final ComponentName mDreamComponent;
135     private final DreamCoordinator mDreamCoordinator;
136 
137     private ProxyServiceConnection mServiceConnection;
138 
139     private IControlledDream mControlledDream;
140 
141     private ArrayList<Integer> mSeenLifecycles = new ArrayList<>();
142 
143     private final Set<Consumer<Integer>> mLifecycleConsumers = new ArraySet<>();
144 
ControlledDreamSession(Context context, ComponentName dreamComponent, DreamCoordinator coordinator)145     public ControlledDreamSession(Context context, ComponentName dreamComponent,
146             DreamCoordinator coordinator) {
147         mContext = context;
148         mDreamComponent = dreamComponent;
149         mDreamCoordinator = coordinator;
150     }
151 
152     private IDreamLifecycleListener mLifecycleListener = new IDreamLifecycleListener.Stub() {
153         public void onAttachedToWindow(IControlledDream dream) {
154             pushLifecycle(DREAM_LIFECYCLE_ON_ATTACHED_TO_WINDOW);
155         }
156 
157         public void onDreamingStarted(IControlledDream dream) {
158             pushLifecycle(DREAM_LIFECYCLE_ON_DREAMING_STARTED);
159         }
160 
161         public void onFocusChanged(IControlledDream dream, boolean hasFocus) {
162             pushLifecycle(
163                     hasFocus ? DREAM_LIFECYCLE_ON_FOCUS_GAINED : DREAM_LIFECYCLE_ON_FOCUS_LOST);
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