• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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 com.android.libraries.testing.deviceshadower.internal;
18 
19 import android.content.ContentProvider;
20 import android.os.Looper;
21 
22 import com.android.internal.annotations.VisibleForTesting;
23 import com.android.libraries.testing.deviceshadower.Enums.Distance;
24 import com.android.libraries.testing.deviceshadower.internal.bluetooth.BlueletImpl;
25 import com.android.libraries.testing.deviceshadower.internal.common.NamedRunnable;
26 import com.android.libraries.testing.deviceshadower.internal.common.Scheduler;
27 import com.android.libraries.testing.deviceshadower.internal.nfc.NfcletImpl;
28 import com.android.libraries.testing.deviceshadower.internal.sms.SmsContentProvider;
29 import com.android.libraries.testing.deviceshadower.internal.sms.SmsletImpl;
30 import com.android.libraries.testing.deviceshadower.internal.utils.Logger;
31 
32 import com.google.common.collect.ImmutableList;
33 
34 import org.robolectric.Shadows;
35 import org.robolectric.shadows.ShadowLooper;
36 
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.concurrent.Callable;
42 import java.util.concurrent.ConcurrentHashMap;
43 import java.util.concurrent.ExecutionException;
44 import java.util.concurrent.ExecutorService;
45 import java.util.concurrent.Executors;
46 import java.util.concurrent.Future;
47 import java.util.concurrent.TimeUnit;
48 
49 /**
50  * Proxy to manage internal data models, and help shadows to exchange data.
51  */
52 public class DeviceShadowEnvironmentImpl {
53 
54     private static final Logger LOGGER = Logger.create("DeviceShadowEnvironmentImpl");
55     private static final long SCHEDULER_WAIT_TIMEOUT_MILLIS = 5000L;
56 
57     // ThreadLocal to store local address for each device.
58     private static InheritableThreadLocal<DeviceletImpl> sLocalDeviceletImpl =
59             new InheritableThreadLocal<>();
60 
61     // Devicelets contains all registered devicelet to simulate a device.
62     private static final Map<String, DeviceletImpl> DEVICELETS = new ConcurrentHashMap<>();
63 
64     @VisibleForTesting
65     static final Map<String, ExecutorService> EXECUTORS = new ConcurrentHashMap<>();
66 
67     private static final List<DeviceShadowException> INTERNAL_EXCEPTIONS =
68             Collections.synchronizedList(new ArrayList<DeviceShadowException>());
69 
70     private static final ContentProvider smsContentProvider = new SmsContentProvider();
71 
getDeviceletImpl(String address)72     public static DeviceletImpl getDeviceletImpl(String address) {
73         return DEVICELETS.get(address);
74     }
75 
checkInternalExceptions()76     public static void checkInternalExceptions() {
77         if (INTERNAL_EXCEPTIONS.size() > 0) {
78             for (DeviceShadowException exception : INTERNAL_EXCEPTIONS) {
79                 LOGGER.e("Internal exception", exception);
80             }
81             INTERNAL_EXCEPTIONS.clear();
82             throw new RuntimeException("DeviceShadower has internal exceptions");
83         }
84     }
85 
reset()86     public static void reset() {
87         // reset local devicelet for single device testing
88         sLocalDeviceletImpl.remove();
89         DEVICELETS.clear();
90         BlueletImpl.reset();
91         INTERNAL_EXCEPTIONS.clear();
92     }
93 
await(long timeoutMillis)94     public static boolean await(long timeoutMillis) {
95         boolean schedulerDone = false;
96         try {
97             schedulerDone = Scheduler.await(timeoutMillis);
98         } catch (InterruptedException e) {
99             // no-op.
100         } finally {
101             if (!schedulerDone) {
102                 catchInternalException(new DeviceShadowException("Scheduler not complete"));
103                 for (DeviceletImpl devicelet : DEVICELETS.values()) {
104                     LOGGER.e(
105                             String.format(
106                                     "Device %s\n\tUI: %s\n\tService: %s",
107                                     devicelet.getAddress(),
108                                     devicelet.getUiScheduler(),
109                                     devicelet.getServiceScheduler()));
110                 }
111                 Scheduler.clear();
112             }
113         }
114         for (ExecutorService executor : EXECUTORS.values()) {
115             executor.shutdownNow();
116         }
117         boolean terminateSuccess = true;
118         for (ExecutorService executor : EXECUTORS.values()) {
119             try {
120                 executor.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS);
121             } catch (InterruptedException e) {
122                 terminateSuccess = false;
123             }
124             if (!executor.isTerminated()) {
125                 LOGGER.e("Failed to terminate executor.");
126                 terminateSuccess = false;
127             }
128         }
129         EXECUTORS.clear();
130         return schedulerDone && terminateSuccess;
131     }
132 
hasLocalDeviceletImpl()133     public static boolean hasLocalDeviceletImpl() {
134         return sLocalDeviceletImpl.get() != null;
135     }
136 
getLocalDeviceletImpl()137     public static DeviceletImpl getLocalDeviceletImpl() {
138         return sLocalDeviceletImpl.get();
139     }
140 
getDeviceletImpls()141     public static List<DeviceletImpl> getDeviceletImpls() {
142         return ImmutableList.copyOf(DEVICELETS.values());
143     }
144 
getLocalBlueletImpl()145     public static BlueletImpl getLocalBlueletImpl() {
146         return sLocalDeviceletImpl.get().blueletImpl();
147     }
148 
getBlueletImpl(String address)149     public static BlueletImpl getBlueletImpl(String address) {
150         DeviceletImpl devicelet = getDeviceletImpl(address);
151         return devicelet == null ? null : devicelet.blueletImpl();
152     }
153 
getLocalNfcletImpl()154     public static NfcletImpl getLocalNfcletImpl() {
155         return sLocalDeviceletImpl.get().nfcletImpl();
156     }
157 
getNfcletImpl(String address)158     public static NfcletImpl getNfcletImpl(String address) {
159         DeviceletImpl devicelet = getDeviceletImpl(address);
160         return devicelet == null ? null : devicelet.nfcletImpl();
161     }
162 
getLocalSmsletImpl()163     public static SmsletImpl getLocalSmsletImpl() {
164         return sLocalDeviceletImpl.get().smsletImpl();
165     }
166 
getSmsContentProvider()167     public static ContentProvider getSmsContentProvider() {
168         return smsContentProvider;
169     }
170 
171     @SuppressWarnings("FutureReturnValueIgnored")
addDevice(String address)172     public static DeviceletImpl addDevice(String address) {
173         EXECUTORS.put(address, Executors.newCachedThreadPool());
174 
175         // DeviceShadower keeps track of the "local" device based on the current thread. It uses an
176         // InheritableThreadLocal, so threads created by the current thread also get the same
177         // thread-local value. Add the device on its own thread, to set the thread local for that
178         // thread and its children.
179         try {
180             EXECUTORS
181                     .get(address)
182                     .submit(
183                             () -> {
184                                 DeviceletImpl devicelet = new DeviceletImpl(address);
185                                 DEVICELETS.put(address, devicelet);
186                                 setLocalDevice(address);
187                                 // Ensure these threads are actually created, by posting one empty
188                                 // runnable.
189                                 devicelet.getServiceScheduler()
190                                         .post(NamedRunnable.create("Init", () -> {
191                                         }));
192                                 devicelet.getUiScheduler().post(NamedRunnable.create("Init", () -> {
193                                 }));
194                             })
195                     .get();
196         } catch (InterruptedException | ExecutionException e) {
197             throw new IllegalStateException(e);
198         }
199 
200         return DEVICELETS.get(address);
201     }
202 
removeDevice(String address)203     public static void removeDevice(String address) {
204         DEVICELETS.remove(address);
205         EXECUTORS.remove(address);
206     }
207 
setInterruptibleBluetooth(int identifier)208     public static void setInterruptibleBluetooth(int identifier) {
209         getLocalBlueletImpl().setInterruptible(identifier);
210     }
211 
interruptBluetooth(String address, int identifier)212     public static void interruptBluetooth(String address, int identifier) {
213         getBlueletImpl(address).interrupt(identifier);
214     }
215 
setDistance(String address1, String address2, final Distance distance)216     public static void setDistance(String address1, String address2, final Distance distance) {
217         final DeviceletImpl device1 = getDeviceletImpl(address1);
218         final DeviceletImpl device2 = getDeviceletImpl(address2);
219 
220         Future<Void> result1 = null;
221         Future<Void> result2 = null;
222         if (device1.updateDistance(address2, distance)) {
223             result1 =
224                     run(
225                             address1,
226                             () -> {
227                                 device1.onDistanceChange(device2, distance);
228                                 return null;
229                             });
230         }
231 
232         if (device2.updateDistance(address1, distance)) {
233             result2 =
234                     run(
235                             address2,
236                             () -> {
237                                 device2.onDistanceChange(device1, distance);
238                                 return null;
239                             });
240         }
241 
242         try {
243             if (result1 != null) {
244                 result1.get();
245             }
246             if (result2 != null) {
247                 result2.get();
248             }
249         } catch (InterruptedException | ExecutionException e) {
250             catchInternalException(new DeviceShadowException(e));
251         }
252     }
253 
254     /**
255      * Set local Bluelet for current thread.
256      *
257      * <p>This can be used to convert current running thread to hold a bluelet object, so that unit
258      * test does not have to call BluetoothEnvironment.run() to run code.
259      */
260     @VisibleForTesting
setLocalDevice(String address)261     public static void setLocalDevice(String address) {
262         DeviceletImpl local = DEVICELETS.get(address);
263         if (local == null) {
264             throw new RuntimeException(address + " is not initialized by BluetoothEnvironment");
265         }
266         sLocalDeviceletImpl.set(local);
267     }
268 
run(final String address, final Callable<T> snippet)269     public static <T> Future<T> run(final String address, final Callable<T> snippet) {
270         return EXECUTORS
271                 .get(address)
272                 .submit(
273                         () -> {
274                             DeviceShadowEnvironmentImpl.setLocalDevice(address);
275                             ShadowLooper mainLooper = Shadows.shadowOf(Looper.getMainLooper());
276                             try {
277                                 T result = snippet.call();
278 
279                                 // Avoid idling the main looper in paused mode since doing so is
280                                 // only allowed from the main thread.
281                                 if (!mainLooper.isPaused()) {
282                                     // In Robolectric, runnable doesn't run when posting thread
283                                     // differs from looper thread, idle main looper explicitly to
284                                     // execute posted Runnables.
285                                     ShadowLooper.idleMainLooper();
286                                 }
287 
288                                 // Wait all scheduled runnables complete.
289                                 Scheduler.await(SCHEDULER_WAIT_TIMEOUT_MILLIS);
290                                 return result;
291                             } catch (Exception e) {
292                                 LOGGER.e("Fail to call code on device: " + address, e);
293                                 if (!mainLooper.isPaused()) {
294                                     // reset() is not supported in paused mode.
295                                     mainLooper.reset();
296                                 }
297                                 throw new RuntimeException(e);
298                             }
299                         });
300     }
301 
302     // @CanIgnoreReturnValue
303     // Return value can be ignored because {@link Scheduler} will call
304     // {@link catchInternalException} to catch exceptions, and throw when test completes.
runOnUi(String address, NamedRunnable snippet)305     public static Future<?> runOnUi(String address, NamedRunnable snippet) {
306         Scheduler scheduler = DeviceShadowEnvironmentImpl.getDeviceletImpl(address)
307                 .getUiScheduler();
308         return run(scheduler, address, snippet);
309     }
310 
311     // @CanIgnoreReturnValue
312     // Return value can be ignored because {@link Scheduler} will call
313     // {@link catchInternalException} to catch exceptions, and throw when test completes.
runOnService(String address, NamedRunnable snippet)314     public static Future<?> runOnService(String address, NamedRunnable snippet) {
315         Scheduler scheduler =
316                 DeviceShadowEnvironmentImpl.getDeviceletImpl(address).getServiceScheduler();
317         return run(scheduler, address, snippet);
318     }
319 
320     // @CanIgnoreReturnValue
321     // Return value can be ignored because {@link Scheduler} will call
322     // {@link catchInternalException} to catch exceptions, and throw when test completes.
run( Scheduler scheduler, final String address, final NamedRunnable snippet)323     private static Future<?> run(
324             Scheduler scheduler, final String address, final NamedRunnable snippet) {
325         return scheduler.post(
326                 NamedRunnable.create(
327                         snippet.toString(),
328                         () -> {
329                             DeviceShadowEnvironmentImpl.setLocalDevice(address);
330                             snippet.run();
331                         }));
332     }
333 
334     public static void catchInternalException(Exception exception) {
335         INTERNAL_EXCEPTIONS.add(new DeviceShadowException(exception));
336     }
337 
338     // This is used to test Device Shadower internal.
339     @VisibleForTesting
340     public static void setDeviceletForTest(String address, DeviceletImpl devicelet) {
341         DEVICELETS.put(address, devicelet);
342     }
343 
344     @VisibleForTesting
345     public static void setExecutorForTest(String address) {
346         setExecutorForTest(address, Executors.newCachedThreadPool());
347     }
348 
349     @VisibleForTesting
350     public static void setExecutorForTest(String address, ExecutorService executor) {
351         EXECUTORS.put(address, executor);
352     }
353 }
354