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