aboutsummaryrefslogtreecommitdiff
path: root/shadows/framework/src
diff options
context:
space:
mode:
Diffstat (limited to 'shadows/framework/src')
-rw-r--r--[-rwxr-xr-x]shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java0
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java138
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java139
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java170
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java135
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java143
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java93
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java96
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java140
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java22
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java14
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java11
-rw-r--r--[-rwxr-xr-x]shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java0
-rw-r--r--[-rwxr-xr-x]shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java0
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java72
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java57
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java192
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java328
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java89
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java36
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java13
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java14
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java109
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java22
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java51
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java9
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java17
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java53
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java13
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java19
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java9
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java99
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java110
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java4
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java33
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java11
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java12
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java18
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java64
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java20
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java28
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java24
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java8
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java67
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java182
56 files changed, 2747 insertions, 180 deletions
diff --git a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
index 33276d916..33276d916 100755..100644
--- a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
+++ b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
new file mode 100644
index 000000000..e2b8f0df3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
@@ -0,0 +1,138 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.companion.AssociationInfo;
+import android.net.MacAddress;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Builder for {@link AssociationInfo}. */
+public class AssociationInfoBuilder {
+ private int id;
+ private int userId;
+ private String packageName;
+ private String deviceMacAddress;
+ private CharSequence displayName;
+ private String deviceProfile;
+ private boolean selfManaged;
+ private boolean notifyOnDeviceNearby;
+ private long approvedMs;
+ private long lastTimeConnectedMs;
+
+ private AssociationInfoBuilder() {}
+
+ public static AssociationInfoBuilder newBuilder() {
+ return new AssociationInfoBuilder();
+ }
+
+ public AssociationInfoBuilder setId(int id) {
+ this.id = id;
+ return this;
+ }
+
+ public AssociationInfoBuilder setUserId(int userId) {
+ this.userId = userId;
+ return this;
+ }
+
+ public AssociationInfoBuilder setPackageName(String packageName) {
+ this.packageName = packageName;
+ return this;
+ }
+
+ public AssociationInfoBuilder setDeviceMacAddress(String deviceMacAddress) {
+ this.deviceMacAddress = deviceMacAddress;
+ return this;
+ }
+
+ public AssociationInfoBuilder setDisplayName(CharSequence displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+
+ public AssociationInfoBuilder setDeviceProfile(String deviceProfile) {
+ this.deviceProfile = deviceProfile;
+ return this;
+ }
+
+ public AssociationInfoBuilder setSelfManaged(boolean selfManaged) {
+ this.selfManaged = selfManaged;
+ return this;
+ }
+
+ public AssociationInfoBuilder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby) {
+ this.notifyOnDeviceNearby = notifyOnDeviceNearby;
+ return this;
+ }
+
+ public AssociationInfoBuilder setApprovedMs(long approvedMs) {
+ this.approvedMs = approvedMs;
+ return this;
+ }
+
+ public AssociationInfoBuilder setLastTimeConnectedMs(long lastTimeConnectedMs) {
+ this.lastTimeConnectedMs = lastTimeConnectedMs;
+ return this;
+ }
+
+ public AssociationInfo build() {
+ try {
+ if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+ // We have two different constructors for AssociationInfo across
+ // T branches. aosp has the constructor that takes a new "revoked" parameter.
+ // Since there is not deterministic way to know which branch we are running in,
+ // we will reflect on the class to see if it has the mRevoked member.
+ // Based on the result we will either invoke the constructor with "revoked" or the
+ // one without this parameter.
+ if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) {
+ return ReflectionHelpers.callConstructor(
+ AssociationInfo.class,
+ ClassParameter.from(int.class, id),
+ ClassParameter.from(int.class, userId),
+ ClassParameter.from(String.class, packageName),
+ ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+ ClassParameter.from(CharSequence.class, displayName),
+ ClassParameter.from(String.class, deviceProfile),
+ ClassParameter.from(boolean.class, selfManaged),
+ ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+ ClassParameter.from(boolean.class, false /*revoked only supported in aosp*/),
+ ClassParameter.from(long.class, approvedMs),
+ ClassParameter.from(long.class, lastTimeConnectedMs));
+ } else {
+ return ReflectionHelpers.callConstructor(
+ AssociationInfo.class,
+ ClassParameter.from(int.class, id),
+ ClassParameter.from(int.class, userId),
+ ClassParameter.from(String.class, packageName),
+ ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+ ClassParameter.from(CharSequence.class, displayName),
+ ClassParameter.from(String.class, deviceProfile),
+ ClassParameter.from(boolean.class, selfManaged),
+ ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+ ClassParameter.from(long.class, approvedMs),
+ ClassParameter.from(long.class, lastTimeConnectedMs));
+ }
+ } else {
+ return ReflectionHelpers.callConstructor(
+ AssociationInfo.class,
+ ClassParameter.from(int.class, id),
+ ClassParameter.from(int.class, userId),
+ ClassParameter.from(String.class, packageName),
+ ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+ ClassParameter.from(CharSequence.class, displayName),
+ ClassParameter.from(String.class, deviceProfile),
+ ClassParameter.from(Class.forName("android.companion.AssociatedDevice"), null),
+ ClassParameter.from(boolean.class, selfManaged),
+ ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+ ClassParameter.from(boolean.class, false /*revoked*/),
+ ClassParameter.from(long.class, approvedMs),
+ ClassParameter.from(long.class, lastTimeConnectedMs),
+ ClassParameter.from(int.class, 0 /*systemDataSyncFlags*/));
+ }
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
new file mode 100644
index 000000000..70e54b190
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
@@ -0,0 +1,139 @@
+package org.robolectric.shadows;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Manages remote address connections for {@link ShadowBluetoothGatt} and {@link
+ * ShadowBluetoothGattServer}.
+ */
+final class BluetoothConnectionManager {
+
+ private static volatile BluetoothConnectionManager instance;
+
+ /** Connection metadata for Gatt Server and Client connections. */
+ private static class BluetoothConnectionMetadata {
+ boolean hasGattClientConnection = false;
+ boolean hasGattServerConnection = false;
+
+ void setHasGattClientConnection(boolean hasGattClientConnection) {
+ this.hasGattClientConnection = hasGattClientConnection;
+ }
+
+ void setHasGattServerConnection(boolean hasGattServerConnection) {
+ this.hasGattServerConnection = hasGattServerConnection;
+ }
+
+ boolean hasGattClientConnection() {
+ return hasGattClientConnection;
+ }
+
+ boolean hasGattServerConnection() {
+ return hasGattServerConnection;
+ }
+
+ boolean isConnected() {
+ return hasGattClientConnection || hasGattServerConnection;
+ }
+ }
+
+ private BluetoothConnectionManager() {}
+
+ static BluetoothConnectionManager getInstance() {
+ if (instance == null) {
+ synchronized (BluetoothConnectionManager.class) {
+ if (instance == null) {
+ instance = new BluetoothConnectionManager();
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * Map representing remote address connections, mapping a remote address to a {@link
+ * BluetoothConnectionMetadata}.
+ */
+ private final Map<String, BluetoothConnectionMetadata> remoteAddressConnectionMap =
+ new HashMap<>();
+
+ /**
+ * Register a Gatt Client Connection. Intended for use by {@link
+ * ShadowBluetoothGatt#notifyConnection} when simulating a successful Gatt Client Connection.
+ */
+ void registerGattClientConnection(String remoteAddress) {
+ if (!remoteAddressConnectionMap.containsKey(remoteAddress)) {
+ remoteAddressConnectionMap.put(remoteAddress, new BluetoothConnectionMetadata());
+ }
+ remoteAddressConnectionMap.get(remoteAddress).setHasGattClientConnection(true);
+ }
+
+ /**
+ * Unregister a Gatt Client Connection. Intended for use by {@link
+ * ShadowBluetoothGatt#notifyDisconnection} when simulating a successful Gatt client
+ * disconnection.
+ */
+ void unregisterGattClientConnection(String remoteAddress) {
+ if (remoteAddressConnectionMap.containsKey(remoteAddress)) {
+ remoteAddressConnectionMap.get(remoteAddress).setHasGattClientConnection(false);
+ }
+ }
+
+ /**
+ * Register a Gatt Server Connection. Intended for use by {@link
+ * ShadowBluetoothGattServer#notifyConnection} when simulating a successful Gatt server
+ * connection.
+ */
+ void registerGattServerConnection(String remoteAddress) {
+ if (!remoteAddressConnectionMap.containsKey(remoteAddress)) {
+ remoteAddressConnectionMap.put(remoteAddress, new BluetoothConnectionMetadata());
+ }
+ remoteAddressConnectionMap.get(remoteAddress).setHasGattServerConnection(true);
+ }
+
+ /**
+ * Unregister a Gatt Server Connection. Intended for use by {@link
+ * ShadowBluetoothGattServer#notifyDisconnection} when simulating a successful Gatt server
+ * disconnection.
+ */
+ void unregisterGattServerConnection(String remoteAddress) {
+ if (remoteAddressConnectionMap.containsKey(remoteAddress)) {
+ remoteAddressConnectionMap.get(remoteAddress).setHasGattServerConnection(false);
+ }
+ }
+
+ /**
+ * Returns true if remote address has an active gatt client connection.
+ *
+ * @param remoteAddress remote address
+ */
+ boolean hasGattClientConnection(String remoteAddress) {
+ return remoteAddressConnectionMap.containsKey(remoteAddress)
+ && remoteAddressConnectionMap.get(remoteAddress).hasGattClientConnection();
+ }
+
+ /**
+ * Returns true if remote address has an active gatt server connection.
+ *
+ * @param remoteAddress remote address
+ */
+ boolean hasGattServerConnection(String remoteAddress) {
+ return remoteAddressConnectionMap.containsKey(remoteAddress)
+ && remoteAddressConnectionMap.get(remoteAddress).hasGattServerConnection();
+ }
+
+ /**
+ * Returns true if remote address has an active connection.
+ *
+ * @param remoteAddress remote address
+ */
+ boolean isConnected(String remoteAddress) {
+ return remoteAddressConnectionMap.containsKey(remoteAddress)
+ && remoteAddressConnectionMap.get(remoteAddress).isConnected();
+ }
+
+ /** Clears all connection information */
+ void resetConnections() {
+ this.remoteAddressConnectionMap.clear();
+ }
+} \ No newline at end of file
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java
new file mode 100644
index 000000000..597aef246
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java
@@ -0,0 +1,170 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import android.telephony.ClosedSubscriberGroupInfo;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellIdentityLte}. */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellIdentityLteBuilder {
+
+ @Nullable private String mcc = null;
+ @Nullable private String mnc = null;
+ private int ci = CellInfo.UNAVAILABLE;
+ private int pci = CellInfo.UNAVAILABLE;
+ private int tac = CellInfo.UNAVAILABLE;
+ private int earfcn = CellInfo.UNAVAILABLE;
+ private int[] bands = new int[0];
+ private int bandwidth = CellInfo.UNAVAILABLE;
+ @Nullable private String alphal = null;
+ @Nullable private String alphas = null;
+ private List<String> additionalPlmns = new ArrayList<>();
+
+ private CellIdentityLteBuilder() {}
+
+ public static CellIdentityLteBuilder newBuilder() {
+ return new CellIdentityLteBuilder();
+ }
+
+ protected static CellIdentityLte getDefaultInstance() {
+ return reflector(CellIdentityLteReflector.class).newCellIdentityLte();
+ }
+
+ public CellIdentityLteBuilder setMcc(String mcc) {
+ this.mcc = mcc;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setMnc(String mnc) {
+ this.mnc = mnc;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setCi(int ci) {
+ this.ci = ci;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setPci(int pci) {
+ this.pci = pci;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setTac(int tac) {
+ this.tac = tac;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setEarfcn(int earfcn) {
+ this.earfcn = earfcn;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setBands(int[] bands) {
+ this.bands = bands;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setBandwidth(int bandwidth) {
+ this.bandwidth = bandwidth;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setLongOperatorName(String longOperatorName) {
+ this.alphal = longOperatorName;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setShortOperatorName(String shortOperatorName) {
+ this.alphas = shortOperatorName;
+ return this;
+ }
+
+ public CellIdentityLteBuilder setAdditionalPlmns(List<String> additionalPlmns) {
+ this.additionalPlmns = additionalPlmns;
+ return this;
+ }
+
+ public CellIdentityLte build() {
+ CellIdentityLteReflector cellIdentityLteReflector = reflector(CellIdentityLteReflector.class);
+ int apiLevel = RuntimeEnvironment.getApiLevel();
+ if (apiLevel < Build.VERSION_CODES.N) {
+ return cellIdentityLteReflector.newCellIdentityLte(
+ mccOrMncToInt(mcc), mccOrMncToInt(mnc), ci, pci, tac);
+ } else if (apiLevel < Build.VERSION_CODES.P) {
+ return cellIdentityLteReflector.newCellIdentityLte(
+ mccOrMncToInt(mcc), mccOrMncToInt(mnc), ci, pci, tac, earfcn);
+ } else if (apiLevel < Build.VERSION_CODES.R) {
+ return cellIdentityLteReflector.newCellIdentityLte(
+ ci, pci, tac, earfcn, bandwidth, mcc, mnc, alphal, alphas);
+ } else {
+ return cellIdentityLteReflector.newCellIdentityLte(
+ ci,
+ pci,
+ tac,
+ earfcn,
+ bands,
+ bandwidth,
+ mcc,
+ mnc,
+ alphal,
+ alphas,
+ additionalPlmns,
+ /* csgInfo= */ null);
+ }
+ }
+
+ private static int mccOrMncToInt(@Nullable String mccOrMnc) {
+ return mccOrMnc == null ? CellInfo.UNAVAILABLE : Integer.parseInt(mccOrMnc);
+ }
+
+ @ForType(CellIdentityLte.class)
+ private interface CellIdentityLteReflector {
+ @Constructor
+ CellIdentityLte newCellIdentityLte();
+
+ @Constructor
+ CellIdentityLte newCellIdentityLte(int mcc, int mnc, int ci, int pci, int tac);
+
+ @Constructor
+ CellIdentityLte newCellIdentityLte(int mcc, int mnc, int ci, int pci, int tac, int earfcn);
+
+ @Constructor
+ CellIdentityLte newCellIdentityLte(
+ int ci,
+ int pci,
+ int tac,
+ int earfcn,
+ int bandwidth,
+ String mcc,
+ String mnc,
+ String alphal,
+ String alphas);
+
+ @Constructor
+ CellIdentityLte newCellIdentityLte(
+ int ci,
+ int pci,
+ int tac,
+ int earfcn,
+ int[] bands,
+ int bandwidth,
+ String mcc,
+ String mnc,
+ String alphal,
+ String alphas,
+ Collection<String> additionalPlmns,
+ ClosedSubscriberGroupInfo csgInfo);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java
new file mode 100644
index 000000000..22a0e75c0
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java
@@ -0,0 +1,135 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfo;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellIdentityNr}. */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellIdentityNrBuilder {
+
+ private int pci = CellInfo.UNAVAILABLE;
+ private int tac = CellInfo.UNAVAILABLE;
+ private int nrarfcn = CellInfo.UNAVAILABLE;
+ private int[] bands = new int[0];
+ @Nullable private String mcc = null;
+ @Nullable private String mnc = null;
+ private long nci = CellInfo.UNAVAILABLE;
+ @Nullable private String alphal = null;
+ @Nullable private String alphas = null;
+ private List<String> additionalPlmns = new ArrayList<>();
+
+ private CellIdentityNrBuilder() {}
+
+ public static CellIdentityNrBuilder newBuilder() {
+ return new CellIdentityNrBuilder();
+ }
+
+ // An empty constructor is not available on Q.
+ @RequiresApi(Build.VERSION_CODES.R)
+ protected static CellIdentityNr getDefaultInstance() {
+ return reflector(CellIdentityNrReflector.class).newCellIdentityNr();
+ }
+
+ public CellIdentityNrBuilder setNci(long nci) {
+ this.nci = nci;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setPci(int pci) {
+ this.pci = pci;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setTac(int tac) {
+ this.tac = tac;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setNrarfcn(int nrarfcn) {
+ this.nrarfcn = nrarfcn;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setMcc(String mcc) {
+ this.mcc = mcc;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setMnc(String mnc) {
+ this.mnc = mnc;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setBands(int[] bands) {
+ this.bands = bands;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setLongOperatorName(String longOperatorName) {
+ this.alphal = longOperatorName;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setShortOperatorName(String shortOperatorName) {
+ this.alphas = shortOperatorName;
+ return this;
+ }
+
+ public CellIdentityNrBuilder setAdditionalPlmns(List<String> additionalPlmns) {
+ this.additionalPlmns = additionalPlmns;
+ return this;
+ }
+
+ public CellIdentityNr build() {
+ CellIdentityNrReflector cellIdentityReflector = reflector(CellIdentityNrReflector.class);
+ if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.R) {
+ return cellIdentityReflector.newCellIdentityNr(
+ pci, tac, nrarfcn, mcc, mnc, nci, alphal, alphas);
+ } else {
+ return cellIdentityReflector.newCellIdentityNr(
+ pci, tac, nrarfcn, bands, mcc, mnc, nci, alphal, alphas, additionalPlmns);
+ }
+ }
+
+ @ForType(CellIdentityNr.class)
+ private interface CellIdentityNrReflector {
+
+ @Constructor
+ CellIdentityNr newCellIdentityNr();
+
+ @Constructor
+ CellIdentityNr newCellIdentityNr(
+ int pci,
+ int tac,
+ int nrarfcn,
+ String mcc,
+ String mnc,
+ long nci,
+ String alphal,
+ String alphas);
+
+ @Constructor
+ CellIdentityNr newCellIdentityNr(
+ int pci,
+ int tac,
+ int nrarfcn,
+ int[] bands,
+ String mcc,
+ String mnc,
+ long nci,
+ String alphal,
+ String alphas,
+ Collection<String> additionalPlmns);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
new file mode 100644
index 000000000..6f3f93420
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
@@ -0,0 +1,143 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoLte;
+import android.telephony.CellSignalStrengthLte;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.WithType;
+
+/** Builder for {@link android.telephony.CellInfoLte}. */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellInfoLteBuilder {
+
+ private boolean isRegistered = false;
+ private long timeStamp = 0L;
+ private int cellConnectionStatus = 0;
+ private CellIdentityLte cellIdentity;
+ private CellSignalStrengthLte cellSignalStrength;
+
+ private CellInfoLteBuilder() {}
+
+ public static CellInfoLteBuilder newBuilder() {
+ return new CellInfoLteBuilder();
+ }
+
+ public CellInfoLteBuilder setRegistered(boolean isRegistered) {
+ this.isRegistered = isRegistered;
+ return this;
+ }
+
+ public CellInfoLteBuilder setTimeStampNanos(long timeStamp) {
+ this.timeStamp = timeStamp;
+ return this;
+ }
+
+ public CellInfoLteBuilder setCellConnectionStatus(int cellConnectionStatus) {
+ this.cellConnectionStatus = cellConnectionStatus;
+ return this;
+ }
+
+ public CellInfoLteBuilder setCellIdentity(CellIdentityLte cellIdentity) {
+ this.cellIdentity = cellIdentity;
+ return this;
+ }
+
+ public CellInfoLteBuilder setCellSignalStrength(CellSignalStrengthLte cellSignalStrength) {
+ this.cellSignalStrength = cellSignalStrength;
+ return this;
+ }
+
+ public CellInfoLte build() {
+ int apiLevel = RuntimeEnvironment.getApiLevel();
+ if (cellIdentity == null) {
+ if (apiLevel > Build.VERSION_CODES.Q) {
+ cellIdentity = CellIdentityLteBuilder.getDefaultInstance();
+ } else {
+ cellIdentity = CellIdentityLteBuilder.newBuilder().build();
+ }
+ }
+ if (cellSignalStrength == null) {
+ cellSignalStrength = CellSignalStrengthLteBuilder.getDefaultInstance();
+ }
+ CellInfoLteReflector cellInfoLteReflector = reflector(CellInfoLteReflector.class);
+ if (apiLevel < Build.VERSION_CODES.TIRAMISU) {
+ CellInfoLte cellInfo = cellInfoLteReflector.newCellInfoLte();
+ cellInfoLteReflector = reflector(CellInfoLteReflector.class, cellInfo);
+ cellInfoLteReflector.setCellIdentity(cellIdentity);
+ cellInfoLteReflector.setCellSignalStrength(cellSignalStrength);
+ CellInfoReflector cellInfoReflector = reflector(CellInfoReflector.class, cellInfo);
+ cellInfoReflector.setTimeStamp(timeStamp);
+ if (apiLevel <= Build.VERSION_CODES.KITKAT) {
+ cellInfoReflector.setRegisterd(isRegistered);
+ } else {
+ cellInfoReflector.setRegistered(isRegistered);
+ }
+ if (apiLevel > Build.VERSION_CODES.O_MR1) {
+ cellInfoReflector.setCellConnectionStatus(cellConnectionStatus);
+ }
+ return cellInfo;
+ } else {
+ try {
+ // This reflection is highly brittle but there is currently no choice as CellConfigLte is
+ // entirely @hide.
+ Class cellConfigLteClass = Class.forName("android.telephony.CellConfigLte");
+ return cellInfoLteReflector.newCellInfoLte(
+ cellConnectionStatus,
+ isRegistered,
+ timeStamp,
+ cellIdentity,
+ cellSignalStrength,
+ ReflectionHelpers.callConstructor(cellConfigLteClass));
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @ForType(CellInfoLte.class)
+ private interface CellInfoLteReflector {
+ @Constructor
+ CellInfoLte newCellInfoLte();
+
+ @Constructor
+ CellInfoLte newCellInfoLte(
+ int cellConnectionStatus,
+ boolean isRegistered,
+ long timeStamp,
+ CellIdentityLte cellIdentity,
+ CellSignalStrengthLte cellSignalStrength,
+ @WithType("android.telephony.CellConfigLte") Object cellConfigLte);
+
+ @Accessor("mCellIdentityLte")
+ void setCellIdentity(CellIdentityLte cellIdentity);
+
+ @Accessor("mCellSignalStrengthLte")
+ void setCellSignalStrength(CellSignalStrengthLte cellSignalStrength);
+ }
+
+ @ForType(CellInfo.class)
+ private interface CellInfoReflector {
+
+ // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/kitkat-release/telephony/java/android/telephony/CellInfo.java#79
+ @Accessor("mRegistered")
+ void setRegisterd(boolean registered); // NOTYPO
+
+ @Accessor("mRegistered")
+ void setRegistered(boolean registered);
+
+ @Accessor("mTimeStamp")
+ void setTimeStamp(long registered);
+
+ @Accessor("mCellConnectionStatus")
+ void setCellConnectionStatus(int cellConnectionStatus);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java
new file mode 100644
index 000000000..78195246e
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java
@@ -0,0 +1,93 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.os.Parcel;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfoNr;
+import android.telephony.CellSignalStrengthNr;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellInfoNr}. */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellInfoNrBuilder {
+
+ private boolean isRegistered = false;
+ private long timeStamp = 0L;
+ private int cellConnectionStatus = 0;
+ private CellIdentityNr cellIdentity;
+ private CellSignalStrengthNr cellSignalStrength;
+
+ private CellInfoNrBuilder() {}
+
+ public static CellInfoNrBuilder newBuilder() {
+ return new CellInfoNrBuilder();
+ }
+
+ public CellInfoNrBuilder setRegistered(boolean isRegistered) {
+ this.isRegistered = isRegistered;
+ return this;
+ }
+
+ public CellInfoNrBuilder setTimeStampNanos(long timeStamp) {
+ this.timeStamp = timeStamp;
+ return this;
+ }
+
+ public CellInfoNrBuilder setCellConnectionStatus(int cellConnectionStatus) {
+ this.cellConnectionStatus = cellConnectionStatus;
+ return this;
+ }
+
+ public CellInfoNrBuilder setCellIdentity(CellIdentityNr cellIdentity) {
+ this.cellIdentity = cellIdentity;
+ return this;
+ }
+
+ public CellInfoNrBuilder setCellSignalStrength(CellSignalStrengthNr cellSignalStrength) {
+ this.cellSignalStrength = cellSignalStrength;
+ return this;
+ }
+
+ public CellInfoNr build() {
+ if (cellIdentity == null) {
+ cellIdentity = CellIdentityNrBuilder.getDefaultInstance();
+ }
+ if (cellSignalStrength == null) {
+ cellSignalStrength = CellSignalStrengthNrBuilder.getDefaultInstance();
+ }
+ // CellInfoNr has no default constructor below T so we write it to a Parcel.
+ if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.TIRAMISU) {
+ Parcel p = Parcel.obtain();
+ p.writeInt(/* CellInfo#TYPE_NR */ 6);
+ p.writeInt(isRegistered ? 1 : 0);
+ p.writeLong(timeStamp);
+ p.writeInt(cellConnectionStatus);
+ cellIdentity.writeToParcel(p, 0);
+ cellSignalStrength.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ CellInfoNr cellInfoNr = CellInfoNr.CREATOR.createFromParcel(p);
+ p.recycle();
+ return cellInfoNr;
+ } else {
+ return reflector(CellInfoNrReflector.class)
+ .newCellInfoNr(
+ cellConnectionStatus, isRegistered, timeStamp, cellIdentity, cellSignalStrength);
+ }
+ }
+
+ @ForType(CellInfoNr.class)
+ private interface CellInfoNrReflector {
+ @Constructor
+ CellInfoNr newCellInfoNr(
+ int cellConnectionStatus,
+ boolean isRegistered,
+ long timeStamp,
+ CellIdentityNr cellIdentity,
+ CellSignalStrengthNr cellSignalStrength);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java
new file mode 100644
index 000000000..9b5d1a1ac
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java
@@ -0,0 +1,96 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthLte;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellSignalStrengthLte} */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellSignalStrengthLteBuilder {
+
+ private int rssi = CellInfo.UNAVAILABLE;
+ private int rsrp = CellInfo.UNAVAILABLE;
+ private int rsrq = CellInfo.UNAVAILABLE;
+ private int rssnr = CellInfo.UNAVAILABLE;
+ private int cqiTableIndex = CellInfo.UNAVAILABLE;
+ private int cqi = CellInfo.UNAVAILABLE;
+ private int timingAdvance = CellInfo.UNAVAILABLE;
+
+ private CellSignalStrengthLteBuilder() {}
+
+ public static CellSignalStrengthLteBuilder newBuilder() {
+ return new CellSignalStrengthLteBuilder();
+ }
+
+ protected static CellSignalStrengthLte getDefaultInstance() {
+ return reflector(CellSignalStrengthLteReflector.class).newCellSignalStrength();
+ }
+
+ /** This is equivalent to {@code signalStrength} pre SDK Q. */
+ public CellSignalStrengthLteBuilder setRssi(int rssi) {
+ this.rssi = rssi;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setRsrp(int rsrp) {
+ this.rsrp = rsrp;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setRsrq(int rsrq) {
+ this.rsrq = rsrq;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setRssnr(int rssnr) {
+ this.rssnr = rssnr;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setCqiTableIndex(int cqiTableIndex) {
+ this.cqiTableIndex = cqiTableIndex;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setCqi(int cqi) {
+ this.cqi = cqi;
+ return this;
+ }
+
+ public CellSignalStrengthLteBuilder setTimingAdvance(int timingAdvance) {
+ this.timingAdvance = timingAdvance;
+ return this;
+ }
+
+ public CellSignalStrengthLte build() {
+ CellSignalStrengthLteReflector cellSignalStrengthReflector =
+ reflector(CellSignalStrengthLteReflector.class);
+ if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.S) {
+ return cellSignalStrengthReflector.newCellSignalStrength(
+ rssi, rsrp, rsrq, rssnr, cqi, timingAdvance);
+ } else {
+ return cellSignalStrengthReflector.newCellSignalStrength(
+ rssi, rsrp, rsrq, rssnr, cqiTableIndex, cqi, timingAdvance);
+ }
+ }
+
+ @ForType(CellSignalStrengthLte.class)
+ private interface CellSignalStrengthLteReflector {
+ @Constructor
+ CellSignalStrengthLte newCellSignalStrength();
+
+ @Constructor
+ CellSignalStrengthLte newCellSignalStrength(
+ int rssi, int rsrp, int rsrq, int rssnr, int cqi, int timingAdvance);
+
+ @Constructor
+ CellSignalStrengthLte newCellSignalStrength(
+ int rssi, int rsrp, int rsrq, int rssnr, int cqiTableIndex, int cqi, int timingAdvance);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java
new file mode 100644
index 000000000..4f3f859bc
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java
@@ -0,0 +1,140 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthNr;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellSignalStrengthNr} */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellSignalStrengthNrBuilder {
+
+ private int csiRrsp = CellInfo.UNAVAILABLE;
+ private int csiRsrq = CellInfo.UNAVAILABLE;
+ private int csiSinr = CellInfo.UNAVAILABLE;
+ private int csiCqiTableIndex = CellInfo.UNAVAILABLE;
+ private List<Byte> csiCqiReport = new ArrayList<>();
+ private int ssRsrp = CellInfo.UNAVAILABLE;
+ private int ssRsrq = CellInfo.UNAVAILABLE;
+ private int ssSinr = CellInfo.UNAVAILABLE;
+ private int timingAdvance = CellInfo.UNAVAILABLE;
+
+ private CellSignalStrengthNrBuilder() {}
+
+ public static CellSignalStrengthNrBuilder newBuilder() {
+ return new CellSignalStrengthNrBuilder();
+ }
+
+ protected static CellSignalStrengthNr getDefaultInstance() {
+ return reflector(CellSignalStrengthNrReflector.class).newCellSignalStrengthNr();
+ }
+
+ public CellSignalStrengthNrBuilder setCsiRsrp(int csiRrsp) {
+ this.csiRrsp = csiRrsp;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setCsiRsrq(int csiRsrq) {
+ this.csiRsrq = csiRsrq;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setCsiSinr(int csiSinr) {
+ this.csiSinr = csiSinr;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setCsiCqiTableIndex(int csiCqiTableIndex) {
+ this.csiCqiTableIndex = csiCqiTableIndex;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setCsiCqiReport(List<Byte> csiCqiReport) {
+ this.csiCqiReport = csiCqiReport;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setSsRsrp(int ssRsrp) {
+ this.ssRsrp = ssRsrp;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setSsRsrq(int ssRsrq) {
+ this.ssRsrq = ssRsrq;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setSsSinr(int ssSinr) {
+ this.ssSinr = ssSinr;
+ return this;
+ }
+
+ public CellSignalStrengthNrBuilder setTimingAdvance(int timingAdvance) {
+ this.timingAdvance = timingAdvance;
+ return this;
+ }
+
+ public CellSignalStrengthNr build() {
+ CellSignalStrengthNrReflector cellSignalStrengthReflector =
+ reflector(CellSignalStrengthNrReflector.class);
+ if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.TIRAMISU) {
+ return cellSignalStrengthReflector.newCellSignalStrengthNr(
+ csiRrsp, csiRsrq, csiSinr, ssRsrp, ssRsrq, ssSinr);
+ } else if (RuntimeEnvironment.getApiLevel() == Build.VERSION_CODES.TIRAMISU) {
+ return cellSignalStrengthReflector.newCellSignalStrengthNr(
+ csiRrsp, csiRsrq, csiSinr, csiCqiTableIndex, csiCqiReport, ssRsrp, ssRsrq, ssSinr);
+ } else {
+ return cellSignalStrengthReflector.newCellSignalStrengthNr(
+ csiRrsp,
+ csiRsrq,
+ csiSinr,
+ csiCqiTableIndex,
+ csiCqiReport,
+ ssRsrp,
+ ssRsrq,
+ ssSinr,
+ timingAdvance);
+ }
+ }
+
+ @ForType(CellSignalStrengthNr.class)
+ private interface CellSignalStrengthNrReflector {
+
+ @Constructor
+ CellSignalStrengthNr newCellSignalStrengthNr();
+
+ @Constructor
+ CellSignalStrengthNr newCellSignalStrengthNr(
+ int csRsrp, int csiRsrq, int csiSinr, int ssRsrp, int ssRsrq, int ssSinr);
+
+ @Constructor
+ CellSignalStrengthNr newCellSignalStrengthNr(
+ int csRsrp,
+ int csiRsrq,
+ int csiSinr,
+ int csiCqiTableIndex,
+ List<Byte> csiCqiReport,
+ int ssRsrp,
+ int ssRsrq,
+ int ssSinr);
+
+ @Constructor
+ CellSignalStrengthNr newCellSignalStrengthNr(
+ int csRsrp,
+ int csiRsrq,
+ int csiSinr,
+ int csiCqiTableIndex,
+ List<Byte> csiCqiReport,
+ int ssRsrp,
+ int ssRsrq,
+ int ssSinr,
+ int timingAdvance);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
index 841f7d5f8..459340273 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
@@ -10,6 +10,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecInfo.EncoderCapabilities;
import android.media.MediaCodecInfo.VideoCapabilities;
import android.media.MediaFormat;
+import android.util.Range;
import com.google.common.base.Preconditions;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.util.ReflectionHelpers;
@@ -266,6 +267,17 @@ public class MediaCodecInfoBuilder {
void setFlagsSupported(int flagsSupported);
}
+ /** Accessor interface for {@link VideoCapabilities}'s internals. */
+ @ForType(VideoCapabilities.class)
+ interface VideoCapabilitiesReflector {
+
+ @Accessor("mWidthRange")
+ void setWidthRange(Range<Integer> range);
+
+ @Accessor("mHeightRange")
+ void setHeightRange(Range<Integer> range);
+ }
+
public CodecCapabilities build() {
Preconditions.checkNotNull(mediaFormat, "mediaFormat is not set.");
Preconditions.checkNotNull(profileLevels, "profileLevels is not set.");
@@ -298,6 +310,16 @@ public class MediaCodecInfoBuilder {
if (isVideoCodec) {
VideoCapabilities videoCaps = createDefaultVideoCapabilities(caps, mediaFormat);
+ VideoCapabilitiesReflector videoCapsReflector =
+ Reflector.reflector(VideoCapabilitiesReflector.class, videoCaps);
+ if (mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) {
+ videoCapsReflector.setWidthRange(
+ new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_WIDTH)));
+ }
+ if (mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
+ videoCapsReflector.setHeightRange(
+ new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)));
+ }
capsReflector.setVideoCaps(videoCaps);
} else {
AudioCapabilities audioCaps = createDefaultAudioCapabilities(caps, mediaFormat);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
index 5da1409f3..d23045b24 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
@@ -1,6 +1,7 @@
package org.robolectric.shadows;
import android.os.Build;
+import android.os.Build.VERSION_CODES;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadow.api.ShadowPicker;
@@ -10,6 +11,7 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> {
private Class<? extends T> binaryShadowClass;
private Class<? extends T> binary9ShadowClass;
private Class<? extends T> binary10ShadowClass;
+ private Class<? extends T> binary14ShadowClass;
public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass,
Class<? extends T> binaryShadowClass,
@@ -18,16 +20,19 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> {
this.binaryShadowClass = binaryShadowClass;
this.binary9ShadowClass = binary9ShadowClass;
this.binary10ShadowClass = binary9ShadowClass;
+ this.binary14ShadowClass = binary9ShadowClass;
}
public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass,
Class<? extends T> binaryShadowClass,
Class<? extends T> binary9ShadowClass,
- Class<? extends T> binary10ShadowClass) {
+ Class<? extends T> binary10ShadowClass,
+ Class<? extends T> binary14ShadowClass) {
this.legacyShadowClass = legacyShadowClass;
this.binaryShadowClass = binaryShadowClass;
this.binary9ShadowClass = binary9ShadowClass;
this.binary10ShadowClass = binary10ShadowClass;
+ this.binary14ShadowClass = binary14ShadowClass;
}
@Override
@@ -35,10 +40,11 @@ public class ResourceModeShadowPicker<T> implements ShadowPicker<T> {
if (ShadowAssetManager.useLegacy()) {
return legacyShadowClass;
} else {
- if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
+ if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) {
+ return binary14ShadowClass;
+ } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
return binary10ShadowClass;
- } else
- if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) {
+ } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) {
return binary9ShadowClass;
} else {
return binaryShadowClass;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
index 47f7306a4..1d575a4b9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
@@ -426,10 +426,10 @@ public class ShadowActivity extends ShadowContextThemeWrapper {
@Implementation
protected void runOnUiThread(Runnable action) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- reflector(DirectActivityReflector.class, realActivity).runOnUiThread(action);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
+ } else {
+ reflector(DirectActivityReflector.class, realActivity).runOnUiThread(action);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
index 883dd2cad..70464bf9c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
@@ -26,7 +26,6 @@ import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import javax.annotation.Nonnull;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
@@ -34,6 +33,7 @@ import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.annotation.Resetter;
+import org.robolectric.util.Logger;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
@@ -275,7 +275,12 @@ public class ShadowActivityThread {
@Resetter
public static void reset() {
Object activityThread = RuntimeEnvironment.getActivityThread();
- Objects.requireNonNull(activityThread, "ShadowActivityThread.reset: ActivityThread not set");
- reflector(_ActivityThread_.class, activityThread).getActivities().clear();
+ if (activityThread == null) {
+ Logger.warn(
+ "RuntimeEnvironment.getActivityThread() is null, an error likely occurred during test"
+ + " initialization.");
+ } else {
+ reflector(_ActivityThread_.class, activityThread).getActivities().clear();
+ }
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java
index eb2276c9e..eb2276c9e 100755..100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java
index c0c9c7a8e..c0c9c7a8e 100755..100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
new file mode 100644
index 000000000..8771d6adb
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
@@ -0,0 +1,72 @@
+package org.robolectric.shadows;
+
+
+import android.annotation.Nullable;
+import android.content.res.AssetManager;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+// TODO: update path to released version.
+// transliterated from
+// https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_rXX/core/jni/android_util_AssetManager.cpp
+
+@Implements(
+ value = AssetManager.class,
+ minSdk = ShadowBuild.UPSIDE_DOWN_CAKE,
+ shadowPicker = ShadowAssetManager.Picker.class)
+@SuppressWarnings("NewApi")
+public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 {
+
+ // static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint
+ // mnc,
+ // jstring locale, jint orientation, jint touchscreen, jint
+ // density,
+ // jint keyboard, jint keyboard_hidden, jint navigation,
+ // jint screen_width, jint screen_height,
+ // jint smallest_screen_width_dp, jint screen_width_dp,
+ // jint screen_height_dp, jint screen_layout, jint ui_mode,
+ // jint color_mode, jint major_version) {
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected static void nativeSetConfiguration(
+ long ptr,
+ int mcc,
+ int mnc,
+ @Nullable String locale,
+ int orientation,
+ int touchscreen,
+ int density,
+ int keyboard,
+ int keyboard_hidden,
+ int navigation,
+ int screen_width,
+ int screen_height,
+ int smallest_screen_width_dp,
+ int screen_width_dp,
+ int screen_height_dp,
+ int screen_layout,
+ int ui_mode,
+ int color_mode,
+ int grammaticalGender, // ignore for now?
+ int major_version) {
+ ShadowArscAssetManager10.nativeSetConfiguration(
+ ptr,
+ mcc,
+ mnc,
+ locale,
+ orientation,
+ touchscreen,
+ density,
+ keyboard,
+ keyboard_hidden,
+ navigation,
+ screen_width,
+ screen_height,
+ smallest_screen_width_dp,
+ screen_width_dp,
+ screen_height_dp,
+ screen_layout,
+ ui_mode,
+ color_mode,
+ major_version);
+ }
+}
+// namespace android
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
index 1f6e40ddf..19c5196f0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
@@ -25,7 +25,8 @@ abstract public class ShadowAssetManager {
ShadowLegacyAssetManager.class,
ShadowArscAssetManager.class,
ShadowArscAssetManager9.class,
- ShadowArscAssetManager10.class);
+ ShadowArscAssetManager10.class,
+ ShadowArscAssetManager14.class);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
index 223435110..5f2d4fd3c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
@@ -93,6 +93,7 @@ public class ShadowAudioManager {
private ImmutableList<Object> defaultDevicesForAttributes = ImmutableList.of();
private List<AudioDeviceInfo> inputDevices = new ArrayList<>();
private List<AudioDeviceInfo> outputDevices = new ArrayList<>();
+ private List<AudioDeviceInfo> availableCommunicationDevices = new ArrayList<>();
private AudioDeviceInfo communicationDevice = null;
public ShadowAudioManager() {
@@ -451,6 +452,23 @@ public class ShadowAudioManager {
}
/**
+ * Sets the list of available communication devices represented by {@link AudioDeviceInfo}.
+ *
+ * <p>The previous list of communication devices is replaced and no notifications of the list of
+ * {@link AudioDeviceCallback} is done.
+ *
+ * <p>To add/remove devices one by one and trigger notifications for the list of {@link
+ * AudioDeviceCallback} please use one of the following methods {@link
+ * #addOutputDevice(AudioDeviceInfo, boolean)}, {@link #removeOutputDevice(AudioDeviceInfo,
+ * boolean)}.
+ */
+ @TargetApi(VERSION_CODES.S)
+ public void setAvailableCommunicationDevices(
+ List<AudioDeviceInfo> availableCommunicationDevices) {
+ this.availableCommunicationDevices = new ArrayList<>(availableCommunicationDevices);
+ }
+
+ /**
* Adds an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if
* the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}.
*/
@@ -497,6 +515,36 @@ public class ShadowAudioManager {
}
/**
+ * Adds an available communication {@link AudioDeviceInfo} and notifies the list of {@link
+ * AudioDeviceCallback} if the device was not present before and indicated by {@code
+ * notifyAudioDeviceCallbacks}.
+ */
+ @TargetApi(VERSION_CODES.S)
+ public void addAvailableCommunicationDevice(
+ AudioDeviceInfo communicationDevice, boolean notifyAudioDeviceCallbacks) {
+ boolean changed =
+ !this.availableCommunicationDevices.contains(communicationDevice)
+ && this.availableCommunicationDevices.add(communicationDevice);
+ if (changed && notifyAudioDeviceCallbacks) {
+ notifyAudioDeviceCallbacks(ImmutableList.of(communicationDevice), /* added= */ true);
+ }
+ }
+
+ /**
+ * Removes an available communication {@link AudioDeviceInfo} and notifies the list of {@link
+ * AudioDeviceCallback} if the device was present before and indicated by {@code
+ * notifyAudioDeviceCallbacks}.
+ */
+ @TargetApi(VERSION_CODES.S)
+ public void removeAvailableCommunicationDevice(
+ AudioDeviceInfo communicationDevice, boolean notifyAudioDeviceCallbacks) {
+ boolean changed = this.availableCommunicationDevices.remove(communicationDevice);
+ if (changed && notifyAudioDeviceCallbacks) {
+ notifyAudioDeviceCallbacks(ImmutableList.of(communicationDevice), /* added= */ false);
+ }
+ }
+
+ /**
* Registers an {@link AudioDeviceCallback} object to receive notifications of changes to the set
* of connected audio devices.
*
@@ -504,8 +552,10 @@ public class ShadowAudioManager {
*
* @see #addInputDevice(AudioDeviceInfo, boolean)
* @see #addOutputDevice(AudioDeviceInfo, boolean)
+ * @see #addAvailableCommunicationDevice(AudioDeviceInfo, boolean)
* @see #removeInputDevice(AudioDeviceInfo, boolean)
* @see #removeOutputDevice(AudioDeviceInfo, boolean)
+ * @see #removeAvailableCommunicationDevice(AudioDeviceInfo, boolean)
*/
@Implementation(minSdk = M)
protected void registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler) {
@@ -520,8 +570,10 @@ public class ShadowAudioManager {
*
* @see #addInputDevice(AudioDeviceInfo, boolean)
* @see #addOutputDevice(AudioDeviceInfo, boolean)
+ * @see #addAvailableCommunicationDevice(AudioDeviceInfo, boolean)
* @see #removeInputDevice(AudioDeviceInfo, boolean)
* @see #removeOutputDevice(AudioDeviceInfo, boolean)
+ * @see #removeAvailableCommunicationDevice(AudioDeviceInfo, boolean)
*/
@Implementation(minSdk = M)
protected void unregisterAudioDeviceCallback(AudioDeviceCallback callback) {
@@ -563,6 +615,11 @@ public class ShadowAudioManager {
this.communicationDevice = null;
}
+ @Implementation(minSdk = S)
+ protected List<AudioDeviceInfo> getAvailableCommunicationDevices() {
+ return availableCommunicationDevices;
+ }
+
@Implementation(minSdk = M)
public AudioDeviceInfo[] getDevices(int flags) {
List<AudioDeviceInfo> result = new ArrayList<>();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
index 5b051405f..c1e78be00 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
@@ -3,10 +3,23 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static com.google.common.base.Preconditions.checkNotNull;
+import android.annotation.NonNull;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
import android.media.AudioSystem;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Table;
+import com.google.common.collect.Tables;
+import java.util.Optional;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
/** Shadow for {@link AudioSystem}. */
@Implements(value = AudioSystem.class, isInAndroidSdk = false)
@@ -17,6 +30,33 @@ public class ShadowAudioSystem {
private static final int MAX_SAMPLE_RATE = 192000;
private static final int MIN_SAMPLE_RATE = 4000;
+ /**
+ * Table to store key-pair of {@link AudioFormat} and {@link AudioAttributes#getUsage()} with
+ * value of support for Direct Playback. Used with {@link #setDirectPlaybackSupport(AudioFormat,
+ * AudioAttributes, int)}, and {@link #getDirectPlaybackSupport(AudioFormat, AudioAttributes)}.
+ */
+ private static final Table<AudioFormat, Integer, Integer> directPlaybackSupportTable =
+ Tables.synchronizedTable(HashBasedTable.create());
+ /**
+ * Table to store pair of {@link OffloadSupportFormat} and {@link
+ * AudioAttributes#getVolumeControlStream()} with a value of Offload Playback support. Used with
+ * {@link #native_get_offload_support}. The table uses {@link OffloadSupportFormat} rather than
+ * {@link AudioFormat} because {@link #native_get_offload_support} does not pass all the fields
+ * needed to reliably reconstruct {@link AudioFormat} instances.
+ */
+ private static final Table<OffloadSupportFormat, Integer, Integer> offloadPlaybackSupportTable =
+ Tables.synchronizedTable(HashBasedTable.create());
+
+ /**
+ * Multimap to store whether a pair of {@link OffloadSupportFormat} and {@link
+ * AudioAttributes#getVolumeControlStream()} ()} support offloaded playback. Used with {@link
+ * #native_is_offload_supported}. The map uses {@link OffloadSupportFormat} keys rather than
+ * {@link AudioFormat} because {@link #native_is_offload_supported} does not pass all the fields
+ * needed to reliably reconstruct {@link AudioFormat} instances.
+ */
+ private static final Multimap<OffloadSupportFormat, Integer> offloadSupportedMap =
+ Multimaps.synchronizedMultimap(HashMultimap.create());
+
@Implementation(minSdk = S)
protected static int native_getMaxChannelCount() {
return MAX_CHANNEL_COUNT;
@@ -38,4 +78,156 @@ public class ShadowAudioSystem {
// https://cs.android.com/android/platform/superproject/+/master:system/media/audio/include/system/audio-base.h;l=197;drc=c84ca89fa5d660046364897482b202c797c8595e
return 8;
}
+
+ /**
+ * Sets direct playback support for a key-pair of {@link AudioFormat} and {@link AudioAttributes}.
+ * As a result, calling {@link #getDirectPlaybackSupport} with the same pair of {@link
+ * AudioFormat} and {@link AudioAttributes} values will return the cached support value.
+ *
+ * @param format the audio format (codec, sample rate, channels)
+ * @param attr the {@link AudioAttributes} to be used for playback
+ * @param directPlaybackSupport the level of direct playback support to save for the format and
+ * attribute pair. Must be one of {@link AudioSystem#DIRECT_NOT_SUPPORTED}, {@link
+ * AudioSystem#OFFLOAD_NOT_SUPPORTED}, {@link AudioSystem#OFFLOAD_SUPPORTED}, {@link
+ * AudioSystem#OFFLOAD_GAPLESS_SUPPORTED}, or a combination of {@link
+ * AudioSystem#DIRECT_OFFLOAD_SUPPORTED}, {@link AudioSystem#DIRECT_OFFLOAD_GAPLESS_SUPPORTED}
+ * and {@link AudioSystem#DIRECT_BITSTREAM_SUPPORTED}
+ */
+ public static void setDirectPlaybackSupport(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr, int directPlaybackSupport) {
+ checkNotNull(format, "Illegal null AudioFormat");
+ checkNotNull(attr, "Illegal null AudioAttributes");
+ directPlaybackSupportTable.put(format, attr.getUsage(), directPlaybackSupport);
+ }
+
+ /**
+ * Retrieves the stored direct playback support for the {@link AudioFormat} and {@link
+ * AudioAttributes}. If no value was stored for the key-pair then {@link
+ * AudioSystem#DIRECT_NOT_SUPPORTED} is returned.
+ *
+ * @param format the audio format (codec, sample rate, channels) to be used for playback
+ * @param attr the {@link AudioAttributes} to be used for playback
+ * @return the level of direct playback playback support for the format and attributes.
+ */
+ @Implementation(minSdk = TIRAMISU)
+ protected static int getDirectPlaybackSupport(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr) {
+ return Optional.ofNullable(directPlaybackSupportTable.get(format, attr.getUsage()))
+ .orElse(AudioSystem.DIRECT_NOT_SUPPORTED);
+ }
+
+ /**
+ * Sets offload playback support for a key-pair of {@link AudioFormat} and {@link
+ * AudioAttributes}. As a result, calling {@link AudioSystem#getOffloadSupport} with the same pair
+ * of {@link AudioFormat} and {@link AudioAttributes} values will return the cached support value.
+ *
+ * @param format the audio format (codec, sample rate, channels)
+ * @param attr the {@link AudioAttributes} to be used for playback
+ * @param offloadSupport the level of offload playback support to save for the format and
+ * attribute pair. Must be one of {@link AudioSystem#OFFLOAD_NOT_SUPPORTED}, {@link
+ * AudioSystem#OFFLOAD_SUPPORTED} or {@link AudioSystem#OFFLOAD_GAPLESS_SUPPORTED}.
+ */
+ public static void setOffloadPlaybackSupport(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr, int offloadSupport) {
+ checkNotNull(format, "Illegal null AudioFormat");
+ checkNotNull(attr, "Illegal null AudioAttributes");
+ offloadPlaybackSupportTable.put(
+ new OffloadSupportFormat(
+ format.getEncoding(),
+ format.getSampleRate(),
+ format.getChannelMask(),
+ format.getChannelIndexMask()),
+ attr.getVolumeControlStream(),
+ offloadSupport);
+ }
+
+ /**
+ * Sets whether offload playback is supported for a key-pair of {@link AudioFormat} and {@link
+ * AudioAttributes}. As a result, calling {@link AudioSystem#isOffloadSupported} with the same
+ * pair of {@link AudioFormat} and {@link AudioAttributes} values will return {@code supported}.
+ *
+ * @param format the audio format (codec, sample rate, channels)
+ * @param attr the {@link AudioAttributes} to be used for playback
+ */
+ public static void setOffloadSupported(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr, boolean supported) {
+ OffloadSupportFormat offloadSupportFormat =
+ new OffloadSupportFormat(
+ format.getEncoding(),
+ format.getSampleRate(),
+ format.getChannelMask(),
+ format.getChannelIndexMask());
+ if (supported) {
+ offloadSupportedMap.put(offloadSupportFormat, attr.getVolumeControlStream());
+ } else {
+ offloadSupportedMap.remove(offloadSupportFormat, attr.getVolumeControlStream());
+ }
+ }
+
+ @Implementation(minSdk = Q, maxSdk = R)
+ protected static boolean native_is_offload_supported(
+ int encoding, int sampleRate, int channelMask, int channelIndexMask, int streamType) {
+ return offloadSupportedMap.containsEntry(
+ new OffloadSupportFormat(encoding, sampleRate, channelMask, channelIndexMask), streamType);
+ }
+
+ @Implementation(minSdk = S)
+ protected static int native_get_offload_support(
+ int encoding, int sampleRate, int channelMask, int channelIndexMask, int streamType) {
+ return Optional.ofNullable(
+ offloadPlaybackSupportTable.get(
+ new OffloadSupportFormat(encoding, sampleRate, channelMask, channelIndexMask),
+ streamType))
+ .orElse(AudioSystem.OFFLOAD_NOT_SUPPORTED);
+ }
+
+ @Resetter
+ public static void reset() {
+ directPlaybackSupportTable.clear();
+ offloadPlaybackSupportTable.clear();
+ offloadSupportedMap.clear();
+ }
+
+ /**
+ * Struct to hold specific values from {@link AudioFormat} which are used in {@link
+ * #native_get_offload_support} and {@link #native_is_offload_supported}.
+ */
+ private static class OffloadSupportFormat {
+ public final int encoding;
+ public final int sampleRate;
+ public final int channelMask;
+ public final int channelIndexMask;
+
+ public OffloadSupportFormat(
+ int encoding, int sampleRate, int channelMask, int channelIndexMask) {
+ this.encoding = encoding;
+ this.sampleRate = sampleRate;
+ this.channelMask = channelMask;
+ this.channelIndexMask = channelIndexMask;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof OffloadSupportFormat)) {
+ return false;
+ }
+ OffloadSupportFormat that = (OffloadSupportFormat) o;
+ return encoding == that.encoding
+ && sampleRate == that.sampleRate
+ && channelMask == that.channelMask
+ && channelIndexMask == that.channelIndexMask;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = encoding;
+ result = 31 * result + sampleRate;
+ result = 31 * result + channelMask;
+ result = 31 * result + channelIndexMask;
+ return result;
+ }
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
index 45a5557a6..6408ce87c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
@@ -1,20 +1,37 @@
package org.robolectric.shadows;
import static android.media.AudioTrack.ERROR_BAD_VALUE;
+import static android.media.AudioTrack.ERROR_DEAD_OBJECT;
import static android.media.AudioTrack.WRITE_BLOCKING;
import static android.media.AudioTrack.WRITE_NON_BLOCKING;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
import android.annotation.NonNull;
+import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.media.AudioTrack.WriteMode;
+import android.media.PlaybackParams;
+import android.os.Build.VERSION;
+import android.os.Parcel;
import android.util.Log;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -50,11 +67,24 @@ public class ShadowAudioTrack {
protected static final int DEFAULT_MIN_BUFFER_SIZE = 1024;
+ // Copied from native code
+ // https://cs.android.com/android/platform/superproject/+/android13-release:frameworks/base/core/jni/android_media_AudioTrack.cpp?q=AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED
+ private static final int AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED = -20;
+
private static final String TAG = "ShadowAudioTrack";
- private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE;
+ /** Direct playback support checked from {@link #native_is_direct_output_supported}. */
+ private static final Multimap<AudioFormatInfo, AudioAttributesInfo> directSupportedFormats =
+ Multimaps.synchronizedMultimap(HashMultimap.create());
+ /** Non-PCM encodings allowed for creating an AudioTrack instance. */
+ private static final Set<Integer> allowedNonPcmEncodings =
+ Collections.synchronizedSet(new HashSet<>());
+
private static final List<OnAudioDataWrittenListener> audioDataWrittenListeners =
new CopyOnWriteArrayList<>();
+ private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE;
+
private int numBytesReceived;
+ private PlaybackParams playbackParams;
@RealObject AudioTrack audioTrack;
/**
@@ -67,6 +97,61 @@ public class ShadowAudioTrack {
minBufferSize = bufferSize;
}
+ /**
+ * Adds support for direct playback for the pair of {@link AudioFormat} and {@link
+ * AudioAttributes} where the format encoding must be non-PCM. Calling {@link
+ * AudioTrack#isDirectPlaybackSupported(AudioFormat, AudioAttributes)} will return {@code true}
+ * for matching {@link AudioFormat} and {@link AudioAttributes}. The matching is performed against
+ * the format's {@linkplain AudioFormat#getEncoding() encoding}, {@linkplain
+ * AudioFormat#getSampleRate() sample rate}, {@linkplain AudioFormat#getChannelMask() channel
+ * mask} and {@linkplain AudioFormat#getChannelIndexMask() channel index mask}, and the
+ * attribute's {@linkplain AudioAttributes#getContentType() content type}, {@linkplain
+ * AudioAttributes#getUsage() usage} and {@linkplain AudioAttributes#getFlags() flags}.
+ *
+ * @param format The {@link AudioFormat}, which must be of a non-PCM encoding. If the encoding is
+ * PCM, the method will throw an {@link IllegalArgumentException}.
+ * @param attr The {@link AudioAttributes}.
+ */
+ public static void addDirectPlaybackSupport(
+ @NonNull AudioFormat format, @NonNull AudioAttributes attr) {
+ checkNotNull(format);
+ checkNotNull(attr);
+ checkArgument(!isPcm(format.getEncoding()));
+
+ directSupportedFormats.put(
+ new AudioFormatInfo(
+ format.getEncoding(),
+ format.getSampleRate(),
+ format.getChannelMask(),
+ format.getChannelIndexMask()),
+ new AudioAttributesInfo(attr.getContentType(), attr.getUsage(), attr.getFlags()));
+ }
+
+ /**
+ * Clears all encodings that have been added for direct playback support with {@link
+ * #addDirectPlaybackSupport}.
+ */
+ public static void clearDirectPlaybackSupportedFormats() {
+ directSupportedFormats.clear();
+ }
+
+ /**
+ * Add a non-PCM encoding for which {@link AudioTrack} instances are allowed to be created.
+ *
+ * @param encoding One of {@link AudioFormat} {@code ENCODING_} constants that represents a
+ * non-PCM encoding. If {@code encoding} is PCM, this method throws an {@link
+ * IllegalArgumentException}.
+ */
+ public static void addAllowedNonPcmEncoding(int encoding) {
+ checkArgument(!isPcm(encoding));
+ allowedNonPcmEncodings.add(encoding);
+ }
+
+ /** Clears all encodings that have been added with {@link #addAllowedNonPcmEncoding(int)}. */
+ public static void clearAllowedNonPcmEncodings() {
+ allowedNonPcmEncodings.clear();
+ }
+
@Implementation(minSdk = N, maxSdk = P)
protected static int native_get_FCC_8() {
// Return the value hard-coded in native code:
@@ -74,6 +159,20 @@ public class ShadowAudioTrack {
return 8;
}
+ @Implementation(minSdk = Q)
+ protected static boolean native_is_direct_output_supported(
+ int encoding,
+ int sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int contentType,
+ int usage,
+ int flags) {
+ return directSupportedFormats.containsEntry(
+ new AudioFormatInfo(encoding, sampleRate, channelMask, channelIndexMask),
+ new AudioAttributesInfo(contentType, usage, flags));
+ }
+
/** Returns a predefined or default minimum buffer size. Audio format and config are neglected. */
@Implementation
protected static int native_get_min_buff_size(
@@ -81,24 +180,141 @@ public class ShadowAudioTrack {
return minBufferSize;
}
+ @Implementation(minSdk = P, maxSdk = Q)
+ protected int native_setup(
+ Object /*WeakReference<AudioTrack>*/ audioTrack,
+ Object /*AudioAttributes*/ attributes,
+ int[] sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int audioFormat,
+ int buffSizeInBytes,
+ int mode,
+ int[] sessionId,
+ long nativeAudioTrack,
+ boolean offload) {
+ // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+ if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+ return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+ }
+ return AudioTrack.SUCCESS;
+ }
+
+ @Implementation(minSdk = R, maxSdk = R)
+ protected int native_setup(
+ Object /*WeakReference<AudioTrack>*/ audioTrack,
+ Object /*AudioAttributes*/ attributes,
+ int[] sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int audioFormat,
+ int buffSizeInBytes,
+ int mode,
+ int[] sessionId,
+ long nativeAudioTrack,
+ boolean offload,
+ int encapsulationMode,
+ Object tunerConfiguration) {
+ // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+ if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+ return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+ }
+ return AudioTrack.SUCCESS;
+ }
+
+ @Implementation(minSdk = S, maxSdk = TIRAMISU)
+ protected int native_setup(
+ Object /*WeakReference<AudioTrack>*/ audioTrack,
+ Object /*AudioAttributes*/ attributes,
+ int[] sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int audioFormat,
+ int buffSizeInBytes,
+ int mode,
+ int[] sessionId,
+ long nativeAudioTrack,
+ boolean offload,
+ int encapsulationMode,
+ Object tunerConfiguration,
+ String opPackageName) {
+ // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+ if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+ return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+ }
+ return AudioTrack.SUCCESS;
+ }
+
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected int native_setup(
+ Object /*WeakReference<AudioTrack>*/ audioTrack,
+ Object /*AudioAttributes*/ attributes,
+ int[] sampleRate,
+ int channelMask,
+ int channelIndexMask,
+ int audioFormat,
+ int buffSizeInBytes,
+ int mode,
+ int[] sessionId,
+ @NonNull Parcel attributionSource,
+ long nativeAudioTrack,
+ boolean offload,
+ int encapsulationMode,
+ Object tunerConfiguration,
+ @NonNull String opPackageName) {
+ // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+ if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+ return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+ }
+ return AudioTrack.SUCCESS;
+ }
+
/**
- * Always return the number of bytes to write. This method returns immedidately even with {@link
- * AudioTrack#WRITE_BLOCKING}
+ * Returns the number of bytes to write. This method returns immediately even with {@link
+ * AudioTrack#WRITE_BLOCKING}. If the {@link AudioTrack} instance was created with a non-PCM
+ * encoding and the encoding can no longer be played directly, the method will return {@link
+ * AudioTrack#ERROR_DEAD_OBJECT};
*/
@Implementation(minSdk = M)
protected final int native_write_byte(
byte[] audioData, int offsetInBytes, int sizeInBytes, int format, boolean isBlocking) {
+ int encoding = audioTrack.getAudioFormat();
+ // Assume that offload support does not change during the lifetime of the instance.
+ if ((VERSION.SDK_INT < 29 || !audioTrack.isOffloadedPlayback())
+ && !isPcm(encoding)
+ && !allowedNonPcmEncodings.contains(encoding)) {
+ return ERROR_DEAD_OBJECT;
+ }
return sizeInBytes;
}
+ @Implementation(minSdk = M)
+ public void setPlaybackParams(@NonNull PlaybackParams params) {
+ playbackParams = checkNotNull(params, "Illegal null params");
+ }
+
+ @Implementation(minSdk = M)
+ @NonNull
+ protected final PlaybackParams getPlaybackParams() {
+ return playbackParams;
+ }
+
/**
- * Always return the number of bytes to write except with invalid parameters. Assumes AudioTrack
- * is already initialized (object properly created). Do not block even if AudioTrack in offload
- * mode is in STOPPING play state. This method returns immediately even with {@link
- * AudioTrack#WRITE_BLOCKING}
+ * Returns the number of bytes to write, except with invalid parameters. If the {@link AudioTrack}
+ * was created for a non-PCM encoding that can no longer be played directly, it returns {@link
+ * AudioTrack#ERROR_DEAD_OBJECT}. Assumes {@link AudioTrack} is already initialized (object
+ * properly created). Do not block even if {@link AudioTrack} in offload mode is in STOPPING play
+ * state. This method returns immediately even with {@link AudioTrack#WRITE_BLOCKING}
*/
@Implementation(minSdk = LOLLIPOP)
protected int write(@NonNull ByteBuffer audioData, int sizeInBytes, @WriteMode int writeMode) {
+ int encoding = audioTrack.getAudioFormat();
+ // Assume that offload support does not change during the lifetime of the instance.
+ if ((VERSION.SDK_INT < 29 || !audioTrack.isOffloadedPlayback())
+ && !isPcm(encoding)
+ && !allowedNonPcmEncodings.contains(encoding)) {
+ return ERROR_DEAD_OBJECT;
+ }
if (writeMode != WRITE_BLOCKING && writeMode != WRITE_NON_BLOCKING) {
Log.e(TAG, "ShadowAudioTrack.write() called with invalid blocking mode");
return ERROR_BAD_VALUE;
@@ -150,5 +366,103 @@ public class ShadowAudioTrack {
@Resetter
public static void resetTest() {
audioDataWrittenListeners.clear();
+ clearDirectPlaybackSupportedFormats();
+ clearAllowedNonPcmEncodings();
+ }
+
+ private static boolean isPcm(int encoding) {
+ switch (encoding) {
+ case AudioFormat.ENCODING_PCM_8BIT:
+ case AudioFormat.ENCODING_PCM_16BIT:
+ case AudioFormat.ENCODING_PCM_24BIT_PACKED:
+ case AudioFormat.ENCODING_PCM_32BIT:
+ case AudioFormat.ENCODING_PCM_FLOAT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Specific fields from {@link AudioFormat} that are used for detection of direct playback
+ * support.
+ *
+ * @see #native_is_direct_output_supported
+ */
+ private static class AudioFormatInfo {
+ private final int encoding;
+ private final int sampleRate;
+ private final int channelMask;
+ private final int channelIndexMask;
+
+ public AudioFormatInfo(int encoding, int sampleRate, int channelMask, int channelIndexMask) {
+ this.encoding = encoding;
+ this.sampleRate = sampleRate;
+ this.channelMask = channelMask;
+ this.channelIndexMask = channelIndexMask;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof AudioFormatInfo)) {
+ return false;
+ }
+
+ AudioFormatInfo other = (AudioFormatInfo) o;
+ return encoding == other.encoding
+ && sampleRate == other.sampleRate
+ && channelMask == other.channelMask
+ && channelIndexMask == other.channelIndexMask;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = encoding;
+ result = 31 * result + sampleRate;
+ result = 31 * result + channelMask;
+ result = 31 * result + channelIndexMask;
+ return result;
+ }
+ }
+
+ /**
+ * Specific fields from {@link AudioAttributes} used for detection of direct playback support.
+ *
+ * @see #native_is_direct_output_supported
+ */
+ private static class AudioAttributesInfo {
+ private final int contentType;
+ private final int usage;
+ private final int flags;
+
+ public AudioAttributesInfo(int contentType, int usage, int flags) {
+ this.contentType = contentType;
+ this.usage = usage;
+ this.flags = flags;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof AudioAttributesInfo)) {
+ return false;
+ }
+
+ AudioAttributesInfo other = (AudioAttributesInfo) o;
+ return contentType == other.contentType && usage == other.usage && flags == other.flags;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = contentType;
+ result = 31 * result + usage;
+ result = 31 * result + flags;
+ return result;
+ }
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
index 737df1fb8..7b6ff7e37 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
@@ -18,6 +18,8 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.UUID;
+import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -40,8 +42,12 @@ public class ShadowBluetoothGatt {
private boolean isClosed = false;
private byte[] writtenBytes;
private byte[] readBytes;
+ // TODO: ShadowBluetoothGatt.services should be removed in favor of just using the real
+ // BluetoothGatt.mServices.
private final Set<BluetoothGattService> discoverableServices = new HashSet<>();
private final ArrayList<BluetoothGattService> services = new ArrayList<>();
+ private final Set<BluetoothGattCharacteristic> characteristicNotificationEnableSet =
+ new HashSet<>();
@RealObject private BluetoothGatt realBluetoothGatt;
@ReflectorObject protected BluetoothGattReflector bluetoothGattReflector;
@@ -185,6 +191,7 @@ public class ShadowBluetoothGatt {
protected boolean discoverServices() {
this.services.clear();
if (!this.discoverableServices.isEmpty()) {
+ // TODO: Don't store the services in the shadow.
this.services.addAll(this.discoverableServices);
if (this.getGattCallback() != null) {
@@ -204,10 +211,39 @@ public class ShadowBluetoothGatt {
*/
@Implementation(minSdk = O)
protected List<BluetoothGattService> getServices() {
+ // TODO: Remove this method when real BluetoothGatt#getServices() works.
return new ArrayList<>(this.services);
}
/**
+ * Overrides {@link BluetoothGatt#getService} to return a service with given UUID.
+ *
+ * @return a service with given UUID that have been discovered through {@link
+ * ShadowBluetoothGatt#discoverServices}.
+ */
+ @Implementation(minSdk = O)
+ @Nullable
+ protected BluetoothGattService getService(UUID uuid) {
+ // TODO: Remove this method when real BluetoothGatt#getService() works.
+ for (BluetoothGattService service : this.services) {
+ if (service.getUuid().equals(uuid)) {
+ return service;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Overrides {@link BluetoothGatt#setCharacteristicNotification} so it returns true (false) if
+ * allowCharacteristicNotification (disallowCharacteristicNotification) is called.
+ */
+ @Implementation(minSdk = O)
+ protected boolean setCharacteristicNotification(
+ BluetoothGattCharacteristic characteristic, boolean enable) {
+ return characteristicNotificationEnableSet.contains(characteristic) == enable;
+ }
+
+ /**
* Reads bytes from incoming characteristic if properties are valid and callback is set. Callback
* responds with {@link BluetoothGattCallback#onCharacteristicWrite} and returns true when
* successful.
@@ -258,6 +294,16 @@ public class ShadowBluetoothGatt {
return true;
}
+ /** Allows the incoming characteristic to be set to enable notification. */
+ public void allowCharacteristicNotification(BluetoothGattCharacteristic characteristic) {
+ characteristicNotificationEnableSet.add(characteristic);
+ }
+
+ /** Disallows the incoming characteristic to be set to enable notification. */
+ public void disallowCharacteristicNotification(BluetoothGattCharacteristic characteristic) {
+ characteristicNotificationEnableSet.remove(characteristic);
+ }
+
public void addDiscoverableService(BluetoothGattService service) {
this.discoverableServices.add(service);
}
@@ -294,6 +340,49 @@ public class ShadowBluetoothGatt {
return this.readBytes;
}
+ public BluetoothConnectionManager getBluetoothConnectionManager() {
+ return BluetoothConnectionManager.getInstance();
+ }
+
+ /**
+ * Simulate a successful Gatt Client Conection with {@link BluetoothConnectionManager}. Performs a
+ * {@link BluetoothGattCallback#onConnectionStateChange} if available.
+ *
+ * @param remoteAddress address of Gatt client
+ */
+ public void notifyConnection(String remoteAddress) {
+ BluetoothConnectionManager.getInstance().registerGattClientConnection(remoteAddress);
+ this.isConnected = true;
+ if (this.isCallbackAppropriate()) {
+ this.getGattCallback()
+ .onConnectionStateChange(
+ this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+ }
+ }
+
+ /**
+ * Simulate a successful Gatt Client Disconnection with {@link BluetoothConnectionManager}.
+ * Performs a {@link BluetoothGattCallback#onConnectionStateChange} if available.
+ *
+ * @param remoteAddress address of Gatt client
+ */
+ public void notifyDisconnection(String remoteAddress) {
+ BluetoothConnectionManager.getInstance().unregisterGattClientConnection(remoteAddress);
+ if (this.isCallbackAppropriate()) {
+ this.getGattCallback()
+ .onConnectionStateChange(
+ this.realBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS,
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+ this.isConnected = false;
+ }
+
+ private boolean isCallbackAppropriate() {
+ return this.getGattCallback() != null && this.isConnected;
+ }
+
+
@ForType(BluetoothGatt.class)
private interface BluetoothGattReflector {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
index 54b96265b..f13928859 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
@@ -3,13 +3,17 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.S;
+import static java.util.stream.Collectors.toCollection;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
@@ -21,7 +25,8 @@ import org.robolectric.annotation.Implements;
@NotThreadSafe
@Implements(value = BluetoothHeadset.class)
public class ShadowBluetoothHeadset {
- private final List<BluetoothDevice> connectedDevices = new ArrayList<>();
+
+ private final Map<BluetoothDevice, Integer> bluetoothDevices = new HashMap<>();
private boolean allowsSendVendorSpecificResultCode = true;
private BluetoothDevice activeBluetoothDevice;
private boolean isVoiceRecognitionSupported = true;
@@ -32,12 +37,29 @@ public class ShadowBluetoothHeadset {
*/
@Implementation
protected List<BluetoothDevice> getConnectedDevices() {
- return connectedDevices;
+ return bluetoothDevices.entrySet().stream()
+ .filter(entry -> entry.getValue() == BluetoothProfile.STATE_CONNECTED)
+ .map(Entry::getKey)
+ .collect(toCollection(ArrayList::new));
}
/** Adds the given BluetoothDevice to the shadow's list of "connected devices" */
public void addConnectedDevice(BluetoothDevice device) {
- connectedDevices.add(device);
+ addDevice(device, BluetoothProfile.STATE_CONNECTED);
+ }
+
+ /**
+ * Adds the provided BluetoothDevice to the shadow profile's device list with an associated
+ * connectionState. The provided connection state will be returned by {@link
+ * ShadowBluetoothHeadset#getConnectionState}.
+ */
+ public void addDevice(BluetoothDevice bluetoothDevice, int connectionState) {
+ bluetoothDevices.put(bluetoothDevice, connectionState);
+ }
+
+ /** Remove the given BluetoothDevice from the shadow profile's device list */
+ public void removeDevice(BluetoothDevice bluetoothDevice) {
+ bluetoothDevices.remove(bluetoothDevice);
}
/**
@@ -49,9 +71,7 @@ public class ShadowBluetoothHeadset {
*/
@Implementation
protected int getConnectionState(BluetoothDevice device) {
- return connectedDevices.contains(device)
- ? BluetoothProfile.STATE_CONNECTED
- : BluetoothProfile.STATE_DISCONNECTED;
+ return bluetoothDevices.getOrDefault(device, BluetoothProfile.STATE_DISCONNECTED);
}
/**
@@ -63,7 +83,7 @@ public class ShadowBluetoothHeadset {
*/
@Implementation
protected boolean startVoiceRecognition(BluetoothDevice bluetoothDevice) {
- if (bluetoothDevice == null || !connectedDevices.contains(bluetoothDevice)) {
+ if (bluetoothDevice == null || !getConnectedDevices().contains(bluetoothDevice)) {
return false;
}
if (activeBluetoothDevice != null) {
@@ -113,7 +133,7 @@ public class ShadowBluetoothHeadset {
if (command == null) {
throw new IllegalArgumentException("Command cannot be null");
}
- return allowsSendVendorSpecificResultCode && connectedDevices.contains(device);
+ return allowsSendVendorSpecificResultCode && getConnectedDevices().contains(device);
}
@Nullable
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
index b0cc137fe..c1c887acc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
@@ -23,6 +23,12 @@ public class ShadowBuild {
private static String serialOverride = Build.UNKNOWN;
/**
+ * Temporary constant for VERSION_CODES.UPSIDE_DOWN_CAKE. Will be removed and replaced once the
+ * constant is available upstream.
+ */
+ public static final int UPSIDE_DOWN_CAKE = 34;
+
+ /**
* Sets the value of the {@link Build#DEVICE} field.
*
* <p>It will be reset for the next test.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
index 55b9b68c6..19be93acc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
@@ -77,7 +77,18 @@ public class ShadowCameraManager {
cameraTorches.put(cameraId, enabled);
}
- @Implementation(minSdk = Build.VERSION_CODES.S)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected CameraDevice openCameraDeviceUserAsync(
+ String cameraId,
+ CameraDevice.StateCallback callback,
+ Executor executor,
+ final int uid,
+ final int oomScoreOffset,
+ boolean overrideToPortrait) {
+ return openCameraDeviceUserAsync(cameraId, callback, executor, uid, oomScoreOffset);
+ }
+
+ @Implementation(minSdk = Build.VERSION_CODES.S, maxSdk = Build.VERSION_CODES.TIRAMISU)
protected CameraDevice openCameraDeviceUserAsync(
String cameraId,
CameraDevice.StateCallback callback,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
index cf1aac20e..a4b1fb79f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
@@ -54,13 +54,13 @@ public abstract class ShadowChoreographer {
* <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
*/
public static void setFrameDelay(Duration delay) {
- checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+ checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
frameDelay = delay;
}
/** See {@link #setFrameDelay(Duration)}. */
public static Duration getFrameDelay() {
- checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+ checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
return frameDelay;
}
@@ -72,13 +72,13 @@ public abstract class ShadowChoreographer {
* <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
*/
public static void setPaused(boolean paused) {
- checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+ checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
isPaused = paused;
}
/** See {@link #setPaused(boolean)}. */
public static boolean isPaused() {
- checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+ checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
return isPaused;
}
@@ -109,11 +109,11 @@ public abstract class ShadowChoreographer {
*/
@Deprecated
public static void setPostFrameCallbackDelay(int delayMillis) {
- if (looperMode() == Mode.PAUSED) {
+ if (looperMode() == Mode.LEGACY) {
+ ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
+ } else {
setPaused(delayMillis != 0);
setFrameDelay(Duration.ofMillis(delayMillis == 0 ? 1 : delayMillis));
- } else {
- ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
index 933024646..75566ce68 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
@@ -12,6 +12,7 @@ import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.os.MessageQueue;
import android.os.SystemClock;
@@ -29,9 +30,8 @@ import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.res.android.NativeObjRegistry;
import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Constructor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;
@@ -86,7 +86,7 @@ public class ShadowDisplayEventReceiver {
new NativeDisplayEventReceiver(new WeakReference<>((DisplayEventReceiver) receiver)));
}
- @Implementation(minSdk = R)
+ @Implementation(minSdk = R, maxSdk = TIRAMISU)
protected static long nativeInit(
WeakReference<DisplayEventReceiver> receiver,
MessageQueue msgQueue,
@@ -95,11 +95,21 @@ public class ShadowDisplayEventReceiver {
return nativeInit(receiver, msgQueue);
}
- @Implementation(minSdk = KITKAT_WATCH)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected static long nativeInit(
+ WeakReference<DisplayEventReceiver> receiver,
+ WeakReference<Object> vsyncEventData,
+ MessageQueue msgQueue,
+ int vsyncSource,
+ int eventRegistration,
+ long layerHandle) {
+ return nativeInit(receiver, msgQueue);
+ }
+
+ @Implementation(minSdk = KITKAT_WATCH, maxSdk = TIRAMISU)
protected static void nativeDispose(long receiverPtr) {
NativeDisplayEventReceiver receiver = nativeObjRegistry.unregister(receiverPtr);
if (receiver != null) {
- receiver.dispose();
}
}
@@ -141,24 +151,11 @@ public class ShadowDisplayEventReceiver {
displayEventReceiverReflector.onVsync(
ShadowSystem.nanoTime(), 0L, /* SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN */ 1);
} else if (RuntimeEnvironment.getApiLevel() < TIRAMISU) {
- try {
- // onVsync takes a package-private VSyncData class as a parameter, thus reflection
- // needs to be used
- Object vsyncData =
- ReflectionHelpers.callConstructor(
- Class.forName("android.view.DisplayEventReceiver$VsyncEventData"),
- ClassParameter.from(long.class, 1), /* id */
- ClassParameter.from(long.class, 10), /* frameDeadline */
- ClassParameter.from(long.class, 1)); /* frameInterval */
-
- displayEventReceiverReflector.onVsync(
- ShadowSystem.nanoTime(),
- 0L, /* physicalDisplayId currently ignored */
- /* frame= */ 1,
- vsyncData /* VsyncEventData */);
- } catch (ClassNotFoundException e) {
- throw new LinkageError("Unable to construct VsyncEventData", e);
- }
+ displayEventReceiverReflector.onVsync(
+ ShadowSystem.nanoTime(),
+ 0L, /* physicalDisplayId currently ignored */
+ /* frame= */ 1,
+ newVsyncEventData() /* VsyncEventData */);
} else {
displayEventReceiverReflector.onVsync(
ShadowSystem.nanoTime(),
@@ -240,6 +237,11 @@ public class ShadowDisplayEventReceiver {
}
private static Object /* VsyncEventData */ newVsyncEventData() {
+ VsyncEventDataReflector vsyncEventDataReflector = reflector(VsyncEventDataReflector.class);
+ if (RuntimeEnvironment.getApiLevel() < TIRAMISU) {
+ return vsyncEventDataReflector.newVsyncEventData(
+ /* id= */ 1, /* frameDeadline= */ 10, /* frameInterval= */ 1);
+ }
try {
// onVsync on T takes a package-private VsyncEventData class, which is itself composed of a
// package private VsyncEventData.FrameTimeline class. So use reflection to build these up
@@ -247,33 +249,26 @@ public class ShadowDisplayEventReceiver {
Class.forName("android.view.DisplayEventReceiver$VsyncEventData$FrameTimeline");
int timelineArrayLength = RuntimeEnvironment.getApiLevel() == TIRAMISU ? 1 : 7;
-
+ FrameTimelineReflector frameTimelineReflector = reflector(FrameTimelineReflector.class);
Object timelineArray = Array.newInstance(frameTimelineClass, timelineArrayLength);
for (int i = 0; i < timelineArrayLength; i++) {
- Array.set(timelineArray, i, newFrameTimeline(frameTimelineClass));
+ Array.set(timelineArray, i, frameTimelineReflector.newFrameTimeline(1, 1, 10));
+ }
+ if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+ return vsyncEventDataReflector.newVsyncEventData(
+ timelineArray, /* preferredFrameTimelineIndex= */ 0, /* frameInterval= */ 1);
+ } else {
+ return vsyncEventDataReflector.newVsyncEventData(
+ timelineArray,
+ /* preferredFrameTimelineIndex= */ 0,
+ timelineArrayLength,
+ /* frameInterval= */ 1);
}
-
- // get FrameTimeline[].class
- Class<?> frameTimeLineArrayClass =
- Class.forName("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;");
- return ReflectionHelpers.callConstructor(
- Class.forName("android.view.DisplayEventReceiver$VsyncEventData"),
- ClassParameter.from(frameTimeLineArrayClass, timelineArray),
- ClassParameter.from(int.class, 0), /* frameDeadline */
- ClassParameter.from(long.class, 1)); /* frameInterval */
} catch (ClassNotFoundException e) {
throw new LinkageError("Unable to construct VsyncEventData", e);
}
}
- private static Object newFrameTimeline(Class<?> frameTimelineClass) {
- return ReflectionHelpers.callConstructor(
- frameTimelineClass,
- ClassParameter.from(long.class, 1) /* vsync id */,
- ClassParameter.from(long.class, 1) /* expectedPresentTime */,
- ClassParameter.from(long.class, 10) /* deadline */);
- }
-
/** Reflector interface for {@link DisplayEventReceiver}'s internals. */
@ForType(DisplayEventReceiver.class)
protected interface DisplayEventReceiverReflector {
@@ -295,5 +290,35 @@ public class ShadowDisplayEventReceiver {
@Accessor("mCloseGuard")
CloseGuard getCloseGuard();
+
+ @Accessor("mReceiverPtr")
+ long getReceiverPtr();
+ }
+
+ @ForType(className = "android.view.DisplayEventReceiver$VsyncEventData")
+ interface VsyncEventDataReflector {
+ @Constructor
+ Object newVsyncEventData(long id, long frameDeadline, long frameInterval);
+
+ @Constructor
+ Object newVsyncEventData(
+ @WithType("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;")
+ Object frameTimelineArray,
+ int preferredFrameTimelineIndex,
+ long frameInterval);
+
+ @Constructor
+ Object newVsyncEventData(
+ @WithType("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;")
+ Object frameTimelineArray,
+ int preferredFrameTimelineIndex,
+ int timelineArrayLength,
+ long frameInterval);
+ }
+
+ @ForType(className = "android.view.DisplayEventReceiver$VsyncEventData$FrameTimeline")
+ interface FrameTimelineReflector {
+ @Constructor
+ Object newFrameTimeline(long id, long expectedPresentTime, long deadline);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
index 9f7957303..1aa08b09a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.P;
@@ -28,7 +27,6 @@ import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.annotation.Nullable;
-import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.Bootstrap;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
@@ -88,20 +86,26 @@ public class ShadowDisplayManagerGlobal {
reflector(DisplayManagerGlobalReflector.class, instance);
displayManagerGlobal.setDm(displayManager);
displayManagerGlobal.setLock(new Object());
+ List<Handler> displayListeners = createDisplayListeners();
+ displayManagerGlobal.setDisplayListeners(displayListeners);
+ displayManagerGlobal.setDisplayInfoCache(new SparseArray<>());
+ return instance;
+ }
- List displayListeners = new CopyOnWriteArrayList();
+ private static List<Handler> createDisplayListeners() {
try {
- // TODO: rexhoffman when we have sufficient detection in android dev replace
- // this with a version check.
+ // The type for mDisplayListeners was changed from ArrayList to CopyOnWriteArrayList
+ // in some branches of T and U, so we need to reflect on DisplayManagerGlobal class
+ // to check the type of mDisplayListeners member before initializing appropriately.
Field f = DisplayManagerGlobal.class.getDeclaredField("mDisplayListeners");
if (f.getType().isAssignableFrom(ArrayList.class)) {
- displayListeners = new ArrayList();
+ return new ArrayList<>();
+ } else {
+ return new CopyOnWriteArrayList<>();
}
} catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
}
- displayManagerGlobal.setDisplayListeners(displayListeners);
- displayManagerGlobal.setDisplayInfoCache(new SparseArray<>());
- return instance;
}
@VisibleForTesting
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
index 791ece2ec..58cd55818 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
@@ -10,6 +10,7 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ColorSpace;
+import android.graphics.ColorSpace.Named;
import android.graphics.ImageDecoder;
import android.graphics.ImageDecoder.DecodeException;
import android.graphics.ImageDecoder.Source;
@@ -247,14 +248,16 @@ public class ShadowImageDecoder {
static String ImageDecoder_nGetMimeType(long nativePtr) {
CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);
// return encodedFormatToString(decoder.mCodec.getEncodedFormat());
- throw new UnsupportedOperationException();
+ // TODO: fix this properly. Just hardcode to png for now or just remove GraphicsMode.LEGACY
+ return "image/png";
}
static ColorSpace ImageDecoder_nGetColorSpace(long nativePtr) {
// auto colorType = codec.computeOutputColorType(codec.getInfo().colorType());
// sk_sp<SkColorSpace> colorSpace = codec.computeOutputColorSpace(colorType);
// return GraphicsJNI.getColorSpace(colorSpace, colorType);
- throw new UnsupportedOperationException();
+ // TODO: fix this properly. Just hardcode to SRGB for now or just remove GraphicsMode.LEGACY
+ return ColorSpace.get(Named.SRGB);
}
// native method implementations...
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
index 0654fbc4f..b8564ca40 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.S_V2;
import static android.os.Build.VERSION_CODES.TIRAMISU;
@@ -70,7 +69,7 @@ public class ShadowImageReader {
return nativeImageSetup(image);
}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected int nativeImageSetup(Object /* Image */ image) {
return nativeImageSetup((Image) image);
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
index 50635c8b2..298fabec6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
@@ -1,10 +1,14 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.hardware.input.InputManager;
+import android.util.SparseArray;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
@@ -13,13 +17,18 @@ import android.view.VerifiedKeyEvent;
import android.view.VerifiedMotionEvent;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
/** Shadow for {@link InputManager} */
@Implements(value = InputManager.class, looseSignatures = true)
public class ShadowInputManager {
+ @RealObject InputManager realInputManager;
+
@Implementation
protected boolean injectInputEvent(InputEvent event, int mode) {
// ignore
@@ -37,6 +46,35 @@ public class ShadowInputManager {
return new int[0];
}
+ @Implementation(maxSdk = TIRAMISU)
+ protected void populateInputDevicesLocked() throws ClassNotFoundException {
+ if (ReflectionHelpers.getField(realInputManager, "mInputDevicesChangedListener") == null) {
+ ReflectionHelpers.setField(
+ realInputManager,
+ "mInputDevicesChangedListener",
+ ReflectionHelpers.callConstructor(
+ Class.forName("android.hardware.input.InputManager$InputDevicesChangedListener")));
+ }
+
+ if (getInputDevices() == null) {
+ final int[] ids = realInputManager.getInputDeviceIds();
+
+ SparseArray<InputDevice> inputDevices = new SparseArray<>();
+ for (int i = 0; i < ids.length; i++) {
+ inputDevices.put(ids[i], null);
+ }
+ setInputDevices(inputDevices);
+ }
+ }
+
+ private SparseArray<InputDevice> getInputDevices() {
+ return reflector(InputManagerReflector.class, realInputManager).getInputDevices();
+ }
+
+ private void setInputDevices(SparseArray<InputDevice> devices) {
+ reflector(InputManagerReflector.class, realInputManager).setInputDevices(devices);
+ }
+
/**
* Provides a local java implementation, since the real implementation is in system server +
* native code.
@@ -78,6 +116,17 @@ public class ShadowInputManager {
@Resetter
public static void reset() {
- ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null);
+ if (SDK_INT < ShadowBuild.UPSIDE_DOWN_CAKE) {
+ ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null);
+ }
+ }
+
+ @ForType(InputManager.class)
+ interface InputManagerReflector {
+ @Accessor("mInputDevices")
+ SparseArray<InputDevice> getInputDevices();
+
+ @Accessor("mInputDevices")
+ void setInputDevices(SparseArray<InputDevice> devices);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
index 42baa4c1f..bf87b3a5b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
@@ -2,6 +2,7 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import android.app.Notification;
import android.app.job.JobParameters;
import android.app.job.JobService;
import org.robolectric.annotation.Implementation;
@@ -19,6 +20,14 @@ public class ShadowJobService extends ShadowService {
this.isRescheduleNeeded = needsReschedule;
}
+ /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected void setNotification(
+ JobParameters params,
+ int notificationId,
+ Notification notification,
+ int jobEndNotificationPolicy) {}
+
/**
* Returns whether the job has finished running. When using this shadow this returns true after
* {@link #jobFinished(JobParameters, boolean)} is called.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
index 4b28bafa2..e06f348bc 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
@@ -49,6 +49,7 @@ import org.robolectric.util.reflector.ForType;
public class ShadowLauncherApps {
private List<ShortcutInfo> shortcuts = new ArrayList<>();
private final Multimap<UserHandle, String> enabledPackages = HashMultimap.create();
+ private final Multimap<UserHandle, ComponentName> enabledActivities = HashMultimap.create();
private final Multimap<UserHandle, LauncherActivityInfo> shortcutActivityList =
HashMultimap.create();
private final Multimap<UserHandle, LauncherActivityInfo> activityList = HashMultimap.create();
@@ -100,6 +101,17 @@ public class ShadowLauncherApps {
}
/**
+ * Sets an activity referenced by ComponentName as enabled, to be checked by {@link
+ * #isActivityEnabled(ComponentName, UserHandle)}.
+ *
+ * @param userHandle the user handle to be set.
+ * @param componentName the component name of the activity to be enabled.
+ */
+ public void setActivityEnabled(UserHandle userHandle, ComponentName componentName) {
+ enabledActivities.put(userHandle, componentName);
+ }
+
+ /**
* Adds a {@link LauncherActivityInfo} to be retrieved by {@link
* #getShortcutConfigActivityList(String, UserHandle)}.
*
@@ -219,10 +231,9 @@ public class ShadowLauncherApps {
"This method is not currently supported in Robolectric.");
}
- @Implementation
+ @Implementation(minSdk = L)
protected boolean isActivityEnabled(ComponentName component, UserHandle user) {
- throw new UnsupportedOperationException(
- "This method is not currently supported in Robolectric.");
+ return enabledActivities.containsEntry(user, component);
}
/**
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
index f624b60d5..2fb348ebd 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
@@ -58,7 +58,7 @@ public class ShadowLegacyLooper extends ShadowLooper {
@Resetter
public static synchronized void resetThreadLoopers() {
// do not use looperMode() here, because its cached value might already have been reset
- if (ConfigurationRegistry.get(LooperMode.Mode.class) == LooperMode.Mode.PAUSED) {
+ if (ConfigurationRegistry.get(LooperMode.Mode.class) != LooperMode.Mode.LEGACY) {
// ignore if realistic looper
return;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
index 2a3a61b10..f954ac234 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
@@ -1,20 +1,38 @@
package org.robolectric.shadows;
+import static org.robolectric.shadow.api.Shadow.newInstanceOf;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
import android.app.Application;
import android.app.LoadedApk;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.Build.VERSION_CODES;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
@Implements(value = LoadedApk.class, isInAndroidSdk = false)
public class ShadowLoadedApk {
+ @RealObject private LoadedApk realLoadedApk;
+ private boolean isClassLoaderInitialized = false;
+ private final Object classLoaderLock = new Object();
@Implementation
public ClassLoader getClassLoader() {
+ // The AppComponentFactory was introduced from SDK 28.
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
+ synchronized (classLoaderLock) {
+ if (!isClassLoaderInitialized) {
+ isClassLoaderInitialized = true;
+ tryInitAppComponentFactory(realLoadedApk);
+ }
+ }
+ }
return this.getClass().getClassLoader();
}
@@ -23,6 +41,35 @@ public class ShadowLoadedApk {
return this.getClass().getClassLoader();
}
+ private void tryInitAppComponentFactory(LoadedApk realLoadedApk) {
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
+ ApplicationInfo applicationInfo = realLoadedApk.getApplicationInfo();
+ if (applicationInfo == null || applicationInfo.appComponentFactory == null) {
+ return;
+ }
+ _LoadedApk_ loadedApkReflector = reflector(_LoadedApk_.class, realLoadedApk);
+ if (!loadedApkReflector.getIncludeCode()) {
+ return;
+ }
+ String fullQualifiedClassName =
+ calculateFullQualifiedClassName(
+ applicationInfo.appComponentFactory, applicationInfo.packageName);
+ android.app.AppComponentFactory factory =
+ (android.app.AppComponentFactory) newInstanceOf(fullQualifiedClassName);
+ if (factory == null) {
+ factory = new android.app.AppComponentFactory();
+ }
+ loadedApkReflector.setAppFactory(factory);
+ }
+ }
+
+ private String calculateFullQualifiedClassName(String className, String packageName) {
+ if (packageName == null) {
+ return className;
+ }
+ return className.startsWith(".") ? packageName + className : className;
+ }
+
/** Accessor interface for {@link LoadedApk}'s internals. */
@ForType(LoadedApk.class)
public interface _LoadedApk_ {
@@ -32,5 +79,11 @@ public class ShadowLoadedApk {
@Accessor("mResources")
void setResources(Resources resources);
+
+ @Accessor("mIncludeCode")
+ boolean getIncludeCode();
+
+ @Accessor("mAppComponentFactory")
+ void setAppFactory(Object appFactory);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
index 103907b93..9bef2d193 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.N_MR1;
@@ -409,7 +408,7 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void invalidateByteBufferLocked(
@Nullable ByteBuffer[] buffers, int index, boolean input) {}
@@ -417,14 +416,14 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void validateInputByteBufferLocked(@Nullable ByteBuffer[] buffers, int index) {}
/** Prevents calling Android-only methods on basic ByteBuffer objects. */
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index, boolean input) {}
/**
@@ -442,7 +441,7 @@ public class ShadowMediaCodec {
}
}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void validateOutputByteBufferLocked(
@Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
validateOutputByteBuffer(buffers, index, info);
@@ -452,14 +451,14 @@ public class ShadowMediaCodec {
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void invalidateByteBuffersLocked(@Nullable ByteBuffer[] buffers) {}
/** Prevents attempting to free non-direct ByteBuffer objects. */
@Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void freeByteBuffer(@Nullable ByteBuffer buffer) {}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected void freeByteBufferLocked(@Nullable ByteBuffer buffer) {}
/** Shadows CodecBuffer to prevent attempting to free non-direct ByteBuffer objects. */
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
index 587356009..f08a53c3f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
@@ -37,6 +37,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import org.robolectric.annotation.Implementation;
@@ -112,8 +113,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
private static final Map<DataSource, Exception> exceptions = new HashMap<>();
private static final Map<DataSource, MediaInfo> mediaInfoMap = new HashMap<>();
- private static final MediaInfoProvider DEFAULT_MEDIA_INFO_PROVIDER = mediaInfoMap::get;
- private static MediaInfoProvider mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
+ private static Optional<MediaInfoProvider> mediaInfoProvider = Optional.empty();
@RealObject private MediaPlayer player;
@@ -650,7 +650,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
* @see #setDataSource(DataSource)
*/
public void doSetDataSource(DataSource dataSource) {
- MediaInfo mediaInfo = mediaInfoProvider.get(dataSource);
+ MediaInfo mediaInfo = getMediaInfo(dataSource);
if (mediaInfo == null) {
throw new IllegalArgumentException(
"Don't know what to do with dataSource "
@@ -663,17 +663,16 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
}
public static MediaInfo getMediaInfo(DataSource dataSource) {
- return mediaInfoProvider.get(dataSource);
+ if (mediaInfoMap.containsKey(dataSource)) {
+ return mediaInfoMap.get(dataSource);
+ }
+ return mediaInfoProvider.map(provider -> provider.get(dataSource)).orElse(null);
}
/**
* Adds a {@link MediaInfo} for a {@link DataSource}.
- *
- * <p>This overrides any {@link MediaInfoProvider} previously set by calling {@link
- * #setMediaInfoProvider}, i.e., the provider will not be used for any {@link DataSource}.
*/
public static void addMediaInfo(DataSource dataSource, MediaInfo info) {
- ShadowMediaPlayer.mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
mediaInfoMap.put(dataSource, info);
}
@@ -685,7 +684,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
* {@link MediaInfo} provided by this {@link MediaInfoProvider} will be used instead.
*/
public static void setMediaInfoProvider(MediaInfoProvider mediaInfoProvider) {
- ShadowMediaPlayer.mediaInfoProvider = mediaInfoProvider;
+ ShadowMediaPlayer.mediaInfoProvider = Optional.of(mediaInfoProvider);
}
public static void addException(DataSource dataSource, RuntimeException e) {
@@ -1536,7 +1535,7 @@ public class ShadowMediaPlayer extends ShadowPlayerBase {
@Resetter
public static void resetStaticState() {
createListener = null;
- mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
+ mediaInfoProvider = Optional.empty();
exceptions.clear();
mediaInfoMap.clear();
DataSource.reset();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
index 8b7b8fa86..7b045e836 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.TIRAMISU;
@@ -64,6 +63,12 @@ public class ShadowNativeFontsFontFamily {
return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
}
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+ protected static long nBuild(
+ long builderPtr, String langTags, int variant, boolean isCustomFallback, boolean isDefaultFallback) {
+ return nBuild(builderPtr, langTags, variant, isCustomFallback);
+ }
+
@Implementation
protected static long nGetReleaseNativeFamily() {
return FontFamilyBuilderNatives.nGetReleaseNativeFamily();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
index 94fadb5ab..32d428088 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
@@ -813,7 +813,7 @@ public class ShadowNativePaint {
paintPtr, text, start, count, ctxStart, ctxCount, isRtl, outMetrics);
}
- @Implementation(minSdk = 10000)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected static float nGetRunCharacterAdvance(
long paintPtr,
char[] text,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
index 0f9e44d17..4cdfb4532 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
@@ -221,7 +221,7 @@ public class ShadowNfcAdapter {
}
if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
nfcAdapterReflector.setHasNfcFeature(false);
- if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.CUR_DEVELOPMENT) {
+ if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.TIRAMISU) {
nfcAdapterReflector.setHasBeamFeature(false);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
index 8fec6464a..16a7398f7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
@@ -112,6 +112,15 @@ public class ShadowPaint {
}
@Implementation
+ protected void setStrikeThruText(boolean strikeThruText) {
+ if (strikeThruText) {
+ setFlags(flags | Paint.STRIKE_THRU_TEXT_FLAG);
+ } else {
+ setFlags(flags & ~Paint.STRIKE_THRU_TEXT_FLAG);
+ }
+ }
+
+ @Implementation
protected Shader setShader(Shader shader) {
this.shader = shader;
return shader;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
index 50f8adf62..ee3bef016 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
@@ -12,6 +12,7 @@ import android.os.Message;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
import android.util.Log;
+import com.google.common.base.Preconditions;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
@@ -58,6 +59,8 @@ public final class ShadowPausedLooper extends ShadowLooper {
private static Set<Looper> loopingLoopers =
Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<Looper, Boolean>()));
+ private static boolean ignoreUncaughtExceptions = false;
+
@RealObject private Looper realLooper;
private boolean isPaused = false;
// the Executor that executes looper messages. Must be written to on looper thread
@@ -317,6 +320,51 @@ public final class ShadowPausedLooper extends ShadowLooper {
}
/**
+ * By default Robolectric will put Loopers that throw uncaught exceptions in their loop method
+ * into an error state, where any future posting to the looper's queue will throw an error.
+ *
+ * <p>This API allows you to disable this behavior. Note this is a permanent setting - it is not
+ * reset between tests.
+ *
+ * @deprecated this method only exists to accommodate legacy tests with preexisting issues.
+ * Silently discarding exceptions is not recommended, and can lead to deadlocks.
+ */
+ @Deprecated
+ public static void setIgnoreUncaughtExceptions(boolean shouldIgnore) {
+ ignoreUncaughtExceptions = shouldIgnore;
+ }
+
+ /**
+ * Shadow loop to handle uncaught exceptions. Without this logic an uncaught exception on a looper
+ * thread will cause idle() to deadlock.
+ */
+ @Implementation
+ protected static void loop() {
+ try {
+ reflector(LooperReflector.class).loop();
+ } catch (Exception e) {
+ Looper realLooper = Preconditions.checkNotNull(Looper.myLooper());
+ ShadowPausedMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue());
+
+ if (ignoreUncaughtExceptions) {
+ // ignore
+ } else {
+ shadowQueue.setUncaughtException(e);
+ // release any ControlRunnables currently in queue to prevent deadlocks
+ shadowQueue.drainQueue(
+ input -> {
+ if (input instanceof ControlRunnable) {
+ ((ControlRunnable) input).runLatch.countDown();
+ return true;
+ }
+ return false;
+ });
+ }
+ throw e;
+ }
+ }
+
+ /**
* If the given {@code lastMessageRead} is not null and the queue is now idle, get the idle
* handlers and run them. This synchronization mirrors what happens in the real message queue
* next() method, but does not block after running the idle handlers.
@@ -345,21 +393,40 @@ public final class ShadowPausedLooper extends ShadowLooper {
private abstract static class ControlRunnable implements Runnable {
protected final CountDownLatch runLatch = new CountDownLatch(1);
+ private volatile RuntimeException exception;
- public void waitTillComplete() {
+ @Override
+ public void run() {
+ try {
+ doRun();
+ } catch (RuntimeException e) {
+ if (!ignoreUncaughtExceptions) {
+ exception = e;
+ }
+ throw e;
+ } finally {
+ runLatch.countDown();
+ }
+ }
+
+ protected abstract void doRun() throws RuntimeException;
+
+ public void waitTillComplete() throws RuntimeException {
try {
runLatch.await();
} catch (InterruptedException e) {
Log.w("ShadowPausedLooper", "wait till idle interrupted");
}
+ if (exception != null) {
+ throw exception;
+ }
}
}
private class IdlingRunnable extends ControlRunnable {
@Override
- public void run() {
- try {
+ public void doRun() {
while (true) {
Message msg = getNextExecutableMessage();
if (msg == null) {
@@ -369,26 +436,20 @@ public final class ShadowPausedLooper extends ShadowLooper {
shadowMsg(msg).recycleUnchecked();
triggerIdleHandlersIfNeeded(msg);
}
- } finally {
- runLatch.countDown();
- }
}
}
private class RunOneRunnable extends ControlRunnable {
@Override
- public void run() {
- try {
+ public void doRun() {
+
Message msg = shadowQueue().getNextIgnoringWhen();
if (msg != null) {
SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen());
msg.getTarget().dispatchMessage(msg);
triggerIdleHandlersIfNeeded(msg);
}
- } finally {
- runLatch.countDown();
- }
}
}
@@ -408,6 +469,8 @@ public final class ShadowPausedLooper extends ShadowLooper {
}
looperExecutor.execute(runnable);
runnable.waitTillComplete();
+ // throw immediately if looper died while executing tasks
+ shadowQueue().checkQueueState();
}
}
@@ -422,6 +485,7 @@ public final class ShadowPausedLooper extends ShadowLooper {
@Override
public void execute(Runnable runnable) {
+ shadowQueue().checkQueueState();
executionQueue.add(runnable);
}
@@ -435,18 +499,22 @@ public final class ShadowPausedLooper extends ShadowLooper {
Runnable runnable = executionQueue.take();
runnable.run();
} catch (InterruptedException e) {
- // ignore
+ // ignored
}
}
}
+
+ @Override
+ protected void doRun() throws RuntimeException {
+ throw new UnsupportedOperationException();
+ }
}
private class UnPauseRunnable extends ControlRunnable {
@Override
- public void run() {
+ public void doRun() {
setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
isPaused = false;
- runLatch.countDown();
}
}
@@ -478,5 +546,8 @@ public final class ShadowPausedLooper extends ShadowLooper {
@Direct
void quitSafely();
+
+ @Direct
+ void loop();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
index 162330aad..5caf01642 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
@@ -16,6 +16,9 @@ import android.os.Message;
import android.os.MessageQueue;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import com.google.common.base.Predicate;
import java.time.Duration;
import java.util.ArrayList;
import org.robolectric.RuntimeEnvironment;
@@ -47,6 +50,7 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
new NativeObjRegistry<ShadowPausedMessageQueue>(ShadowPausedMessageQueue.class);
private boolean isPolling = false;
private ShadowPausedSystemClock.Listener clockListener;
+ private Exception uncaughtException = null;
// shadow constructor instead of nativeInit because nativeInit signature has changed across SDK
// versions
@@ -55,7 +59,16 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
invokeConstructor(MessageQueue.class, realQueue, from(boolean.class, quitAllowed));
int ptr = (int) nativeQueueRegistry.register(this);
reflector(MessageQueueReflector.class, realQueue).setPtr(ptr);
- clockListener = () -> nativeWake(ptr);
+ clockListener =
+ () -> {
+ synchronized (realQueue) {
+ // only wake up the Looper thread if queue is non empty to reduce contention if many
+ // Looper threads are active
+ if (getMessages() != null) {
+ nativeWake(ptr);
+ }
+ }
+ };
ShadowPausedSystemClock.addStaticListener(clockListener);
}
@@ -210,8 +223,28 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
return reflector(MessageQueueReflector.class, realQueue).getQuitAllowed();
}
+ @VisibleForTesting
void doEnqueueMessage(Message msg, long when) {
- reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when);
+ enqueueMessage(msg, when);
+ }
+
+ @Implementation
+ protected boolean enqueueMessage(Message msg, long when) {
+ synchronized (realQueue) {
+ if (uncaughtException != null) {
+ // looper thread has died
+ IllegalStateException e =
+ new IllegalStateException(
+ msg.getTarget()
+ + " sending message to a Looper thread that has died due to an uncaught"
+ + " exception",
+ uncaughtException);
+ Log.w("ShadowPausedMessageQueue", e);
+ msg.recycle();
+ throw e;
+ }
+ return reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when);
+ }
}
Message getMessages() {
@@ -283,7 +316,9 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
return Duration.ZERO;
}
while (next != null) {
- when = shadowOfMsg(next).getWhen();
+ if (next.getTarget() != null) {
+ when = shadowOfMsg(next).getWhen();
+ }
next = shadowOfMsg(next).internalGetNext();
}
}
@@ -309,7 +344,9 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
synchronized (realQueue) {
Message next = getMessages();
while (next != null) {
- count++;
+ if (next.getTarget() != null) {
+ count++;
+ }
next = shadowOfMsg(next).internalGetNext();
}
}
@@ -323,12 +360,24 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
*/
Message getNextIgnoringWhen() {
synchronized (realQueue) {
- Message head = getMessages();
- if (head != null) {
- Message next = shadowOfMsg(head).internalGetNext();
- reflector(MessageQueueReflector.class, realQueue).setMessages(next);
+ Message prev = null;
+ Message msg = getMessages();
+ // Head is blocked on synchronization barrier, find next asynchronous message.
+ if (msg != null && msg.getTarget() == null) {
+ do {
+ prev = msg;
+ msg = shadowOfMsg(msg).internalGetNext();
+ } while (msg != null && !msg.isAsynchronous());
}
- return head;
+ if (msg != null) {
+ Message next = shadowOfMsg(msg).internalGetNext();
+ if (prev == null) {
+ reflector(MessageQueueReflector.class, realQueue).setMessages(next);
+ } else {
+ ReflectionHelpers.setField(prev, "next", next);
+ }
+ }
+ return msg;
}
}
@@ -340,6 +389,7 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
msgQueue.setMessages(null);
msgQueue.setIdleHandlers(new ArrayList<>());
msgQueue.setNextBarrierToken(0);
+ setUncaughtException(null);
}
private static ShadowPausedMessage shadowOfMsg(Message head) {
@@ -378,10 +428,50 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
}
}
+ /**
+ * Called when an uncaught exception occurred in this message queue's Looper thread.
+ *
+ * <p>In real android, by default an exception handler is installed which kills the entire process
+ * when an uncaught exception occurs. We don't want to do this in robolectric to isolate tests, so
+ * instead an uncaught exception puts the message queue into an error state, where any future
+ * interaction will rethrow the exception.
+ */
+ void setUncaughtException(Exception e) {
+ synchronized (realQueue) {
+ this.uncaughtException = e;
+ }
+ }
+
+ void checkQueueState() {
+ synchronized (realQueue) {
+ if (uncaughtException != null) {
+ throw new IllegalStateException(
+ "Looper thread that has died due to an uncaught exception", uncaughtException);
+ }
+ }
+ }
+
+ /**
+ * Remove all messages from queue
+ *
+ * @param msgProcessor a callback to apply to each mesg
+ */
+ void drainQueue(Predicate<Runnable> msgProcessor) {
+ synchronized (realQueue) {
+ Message msg = getMessages();
+ while (msg != null) {
+ boolean unused = msgProcessor.apply(msg.getCallback());
+ ShadowMessage shadowMsg = Shadow.extract(msg);
+ msg.recycle();
+ msg = shadowMsg.getNext();
+ }
+ }
+ }
+
/** Accessor interface for {@link MessageQueue}'s internals. */
@ForType(MessageQueue.class)
private interface MessageQueueReflector {
-
+ @Direct
boolean enqueueMessage(Message msg, long when);
Message next();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
index 839e28595..cbf52ae39 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
@@ -41,6 +41,7 @@ import android.content.integrity.IAppIntegrityManager;
import android.content.pm.ICrossProfileApps;
import android.content.pm.IShortcutService;
import android.content.rollback.IRollbackManager;
+import android.hardware.ISensorPrivacyManager;
import android.hardware.biometrics.IAuthService;
import android.hardware.biometrics.IBiometricService;
import android.hardware.fingerprint.IFingerprintService;
@@ -57,6 +58,7 @@ import android.net.IIpSecService;
import android.net.INetworkPolicyManager;
import android.net.INetworkScoreService;
import android.net.ITetheringConnector;
+import android.net.IVpnManager;
import android.net.nsd.INsdManager;
import android.net.vcn.IVcnManagementService;
import android.net.wifi.IWifiManager;
@@ -205,6 +207,8 @@ public class ShadowServiceManager {
addBinderService(Context.UWB_SERVICE, IUwbAdapter.class);
addBinderService(Context.VCN_MANAGEMENT_SERVICE, IVcnManagementService.class);
addBinderService(Context.TRANSLATION_MANAGER_SERVICE, ITranslationManager.class);
+ addBinderService(Context.SENSOR_PRIVACY_SERVICE, ISensorPrivacyManager.class);
+ addBinderService(Context.VPN_MANAGEMENT_SERVICE, IVpnManager.class);
}
if (RuntimeEnvironment.getApiLevel() >= TIRAMISU) {
addBinderService(Context.AMBIENT_CONTEXT_SERVICE, IAmbientContextManager.class);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
index a1895ff6b..c40d24e96 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
@@ -1,6 +1,5 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
@@ -63,7 +62,7 @@ public class ShadowSoundPool {
return 1;
}
- @Implementation(minSdk = CUR_DEVELOPMENT)
+ @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
protected int _play(
int soundID,
float leftVolume,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
index 529aaa405..1b239d188 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
@@ -21,6 +21,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.Executor;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -282,6 +283,17 @@ public class ShadowSubscriptionManager {
}
/**
+ * Adds a listener to a local list of listeners. Will be triggered by {@link
+ * #setActiveSubscriptionInfoList} when the local list of {@link SubscriptionInfo} is updated.
+ */
+ @Implementation(minSdk = R)
+ protected void addOnSubscriptionsChangedListener(
+ Executor executor, OnSubscriptionsChangedListener listener) {
+ listeners.add(listener);
+ listener.onSubscriptionsChanged();
+ }
+
+ /**
* Removes a listener from a local list of listeners. Will be triggered by {@link
* #setActiveSubscriptionInfoList} when the local list of {@link SubscriptionInfo} is updated.
*/
@@ -290,6 +302,16 @@ public class ShadowSubscriptionManager {
listeners.remove(listener);
}
+ /**
+ * Check if a listener exists in the {@link ShadowSubscriptionManager.listeners}.
+ *
+ * @param listener The listener to check.
+ * @return boolean True if the listener already added, otherwise false.
+ */
+ public boolean hasOnSubscriptionsChangedListener(OnSubscriptionsChangedListener listener) {
+ return listeners.contains(listener);
+ }
+
/** Returns subscription Ids that were set via {@link #setActiveSubscriptionInfoList}. */
@Implementation(minSdk = LOLLIPOP_MR1)
@HiddenApi
@@ -405,6 +427,17 @@ public class ShadowSubscriptionManager {
return phoneNumberMap.getOrDefault(subscriptionId, "");
}
+ /**
+ * Returns the phone number for the given {@code subscriptionId}, or an empty string if not
+ * available. {@code source} is ignored and will return the same as {@link #getPhoneNumber(int)}.
+ *
+ * <p>The phone number can be set by {@link #setPhoneNumber(int, String)}
+ */
+ @Implementation(minSdk = TIRAMISU)
+ protected String getPhoneNumber(int subscriptionId, int source) {
+ return getPhoneNumber(subscriptionId);
+ }
+
/** Sets the phone number returned by {@link #getPhoneNumber(int)}. */
public void setPhoneNumber(int subscriptionId, String phoneNumber) {
phoneNumberMap.put(subscriptionId, phoneNumber);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
index bc8528781..63d12e6a1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
@@ -11,6 +11,7 @@ import android.view.SurfaceControl;
import android.view.SurfaceSession;
import dalvik.system.CloseGuard;
import java.util.concurrent.atomic.AtomicInteger;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.ReflectorObject;
@@ -82,6 +83,13 @@ public class ShadowSurfaceControl {
void initializeNativeObject() {
surfaceControlReflector.setNativeObject(nativeObject.incrementAndGet());
+ if (RuntimeEnvironment.getApiLevel() >= ShadowBuild.UPSIDE_DOWN_CAKE) {
+ try {
+ surfaceControlReflector.setFreeNativeResources(() -> {});
+ } catch(Exception e) {
+ // tm branches not yet have mFreeNativeResources added while in partial U state
+ }
+ }
}
@ForType(SurfaceControl.class)
@@ -94,5 +102,8 @@ public class ShadowSurfaceControl {
@Direct
void finalize();
+
+ @Accessor("mFreeNativeResources")
+ void setFreeNativeResources(Runnable runnable);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
index a2bb38aba..b9013704a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
@@ -13,10 +13,10 @@ public class ShadowSystem {
*/
@SuppressWarnings("unused")
public static long nanoTime() {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return TimeUnit.MILLISECONDS.toNanos(SystemClock.uptimeMillis());
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
return ShadowLegacySystemClock.nanoTime();
+ } else {
+ return TimeUnit.MILLISECONDS.toNanos(SystemClock.uptimeMillis());
}
}
@@ -27,10 +27,10 @@ public class ShadowSystem {
*/
@SuppressWarnings("unused")
public static long currentTimeMillis() {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return SystemClock.uptimeMillis();
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
return ShadowLegacySystemClock.currentTimeMillis();
+ } else {
+ return SystemClock.uptimeMillis();
}
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
index cce1990a0..41fbf4e7d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
@@ -15,9 +15,9 @@ import android.media.AudioAttributes;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemVibrator;
-import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.vibrator.VibrationEffectSegment;
+import com.google.common.base.Preconditions;
import java.util.List;
import java.util.Optional;
import org.robolectric.RuntimeEnvironment;
@@ -25,7 +25,8 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
-@Implements(value = SystemVibrator.class, isInAndroidSdk = false)
+/** Shadow for {@link SystemVibrator}. */
+@Implements(value = SystemVibrator.class, isInAndroidSdk = false, looseSignatures = true)
public class ShadowSystemVibrator extends ShadowVibrator {
private final Handler handler = new Handler(Looper.getMainLooper());
@@ -133,11 +134,14 @@ public class ShadowSystemVibrator extends ShadowVibrator {
@Implementation(minSdk = S)
protected void vibrate(
- int uid,
- String opPkg,
- VibrationEffect effect,
- String reason,
- VibrationAttributes attributes) {
+ Object uid, Object opPkg, Object effect, Object reason, Object attributes) {
+ Preconditions.checkArgument(uid instanceof Integer);
+ Preconditions.checkArgument(opPkg == null || opPkg instanceof String);
+ // The SystemVibrator#vibrate needs effect NonNull.
+ Preconditions.checkArgument(effect instanceof VibrationEffect);
+ Preconditions.checkArgument(reason == null || reason instanceof String);
+ // The SystemVibrator#vibrate needs attributes NonNull.
+ Preconditions.checkArgument(attributes instanceof android.os.VibrationAttributes);
if (effect instanceof VibrationEffect.Composed) {
VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) effect;
vibrationAttributesFromLastVibration = attributes;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
index 0864354d9..048abf03b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -52,6 +52,7 @@ import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyManager.CellInfoCallback;
import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.telephony.emergency.EmergencyNumber;
import android.text.TextUtils;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -59,13 +60,16 @@ import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Executor;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.HiddenApi;
@@ -141,6 +145,7 @@ public class ShadowTelephonyManager {
private String visualVoicemailPackageName = null;
private SignalStrength signalStrength;
private boolean dataEnabled = false;
+ private final Set<Integer> dataDisabledReasons = new HashSet<>();
private boolean isRttSupported;
private final List<String> sentDialerSpecialCodes = new ArrayList<>();
private boolean hearingAidCompatibilitySupported = false;
@@ -152,6 +157,7 @@ public class ShadowTelephonyManager {
private VisualVoicemailSmsParams lastVisualVoicemailSmsParams;
private VisualVoicemailSmsFilterSettings visualVoicemailSmsFilterSettings;
private boolean emergencyCallbackMode;
+ private static Map<Integer, List<EmergencyNumber>> emergencyNumbersList;
/**
* Should be {@link TelephonyManager.BootstrapAuthenticationCallback} but this object was
@@ -169,6 +175,7 @@ public class ShadowTelephonyManager {
@Resetter
public static void reset() {
callComposerStatus = 0;
+ emergencyNumbersList = null;
}
@Implementation(minSdk = S)
@@ -263,6 +270,13 @@ public class ShadowTelephonyManager {
}
/** Call state may be specified via {@link #setCallState(int)}. */
+ @Implementation(minSdk = S)
+ protected int getCallStateForSubscription() {
+ checkReadPhoneStatePermission();
+ return callState;
+ }
+
+ /** Call state may be specified via {@link #setCallState(int)}. */
@Implementation
protected int getCallState() {
checkReadPhoneStatePermission();
@@ -1215,12 +1229,39 @@ public class ShadowTelephonyManager {
}
/**
+ * Implementation for {@link TelephonyManager#isDataEnabledForReason}.
+ *
+ * @return True by default, unless reason is set to false with {@link
+ * TelephonyManager#setDataEnabledForReason}.
+ */
+ @Implementation(minSdk = Build.VERSION_CODES.S)
+ protected boolean isDataEnabledForReason(@TelephonyManager.DataEnabledReason int reason) {
+ checkReadPhoneStatePermission();
+ return !dataDisabledReasons.contains(reason);
+ }
+
+ /**
* Implementation for {@link TelephonyManager#setDataEnabled}. Marked as public in order to allow
* it to be used as a test API.
*/
@Implementation(minSdk = Build.VERSION_CODES.O)
public void setDataEnabled(boolean enabled) {
- dataEnabled = enabled;
+ setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER, enabled);
+ }
+
+ /**
+ * Implementation for {@link TelephonyManager#setDataEnabledForReason}. Marked as public in order
+ * to allow it to be used as a test API.
+ */
+ @Implementation(minSdk = Build.VERSION_CODES.S)
+ public void setDataEnabledForReason(
+ @TelephonyManager.DataEnabledReason int reason, boolean enabled) {
+ if (enabled) {
+ dataDisabledReasons.remove(reason);
+ } else {
+ dataDisabledReasons.add(reason);
+ }
+ dataEnabled = dataDisabledReasons.isEmpty();
}
/**
@@ -1374,4 +1415,25 @@ public class ShadowTelephonyManager {
return sentIntent;
}
}
+
+ /**
+ * Sets the emergency numbers list returned by {@link TelephonyManager#getEmergencyNumberList}.
+ */
+ public static void setEmergencyNumberList(
+ Map<Integer, List<EmergencyNumber>> emergencyNumbersList) {
+ ShadowTelephonyManager.emergencyNumbersList = emergencyNumbersList;
+ }
+
+ /**
+ * Implementation for {@link TelephonyManager#getEmergencyNumberList}.
+ *
+ * @return an immutable map by default, unless set with {@link #setEmergencyNumberList}.
+ */
+ @Implementation(minSdk = R)
+ protected Map<Integer, List<EmergencyNumber>> getEmergencyNumberList() {
+ if (ShadowTelephonyManager.emergencyNumbersList != null) {
+ return ShadowTelephonyManager.emergencyNumbersList;
+ }
+ return ImmutableMap.of();
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
index 5c8de7314..00ad65840 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
@@ -600,6 +600,16 @@ public class ShadowUserManager {
}
@HiddenApi
+ @Implementation(minSdk = R)
+ protected List<UserHandle> getUserHandles(boolean excludeDying) {
+ ArrayList<UserHandle> userHandles = new ArrayList<>();
+ for (int id : userManagerState.userSerialNumbers.keySet()) {
+ userHandles.addAll(userManagerState.userProfilesListMap.get(id));
+ }
+ return userHandles;
+ }
+
+ @HiddenApi
@Implementation(minSdk = JELLY_BEAN_MR1)
protected static int getMaxSupportedUsers() {
return maxSupportedUsers;
@@ -998,6 +1008,9 @@ public class ShadowUserManager {
@Implementation(minSdk = JELLY_BEAN_MR1)
protected boolean removeUser(int userHandle) {
+ if (!userManagerState.userInfoMap.containsKey(userHandle)) {
+ return false;
+ }
userManagerState.userInfoMap.remove(userHandle);
userManagerState.userPidMap.remove(userHandle);
userManagerState.userSerialNumbers.remove(userHandle);
@@ -1021,6 +1034,13 @@ public class ShadowUserManager {
return removeUser(user.getIdentifier());
}
+ @Implementation(minSdk = TIRAMISU)
+ protected int removeUserWhenPossible(UserHandle user, boolean overrideDevicePolicy) {
+ return removeUser(user.getIdentifier())
+ ? UserManager.REMOVE_RESULT_REMOVED
+ : UserManager.REMOVE_RESULT_ERROR_UNKNOWN;
+ }
+
@Implementation(minSdk = N)
protected static boolean supportsMultipleUsers() {
return isMultiUserSupported;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
index b66a0a414..276a31db3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
@@ -3,18 +3,19 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.R;
import android.media.AudioAttributes;
-import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
-import android.os.vibrator.VibrationEffectSegment;
+import android.os.vibrator.PrimitiveSegment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
+import org.robolectric.util.ReflectionHelpers;
@Implements(Vibrator.class)
public class ShadowVibrator {
@@ -22,10 +23,10 @@ public class ShadowVibrator {
static boolean cancelled;
static long milliseconds;
protected static long[] pattern;
- protected static final List<VibrationEffectSegment> vibrationEffectSegments = new ArrayList<>();
+ protected static final List<Object> vibrationEffectSegments = new ArrayList<>();
protected static final List<PrimitiveEffect> primitiveEffects = new ArrayList<>();
protected static final List<Integer> supportedPrimitives = new ArrayList<>();
- @Nullable protected static VibrationAttributes vibrationAttributesFromLastVibration;
+ @Nullable protected static Object vibrationAttributesFromLastVibration;
@Nullable protected static AudioAttributes audioAttributesFromLastVibration;
static int repeat;
static boolean hasVibrator = true;
@@ -81,9 +82,18 @@ public class ShadowVibrator {
return repeat;
}
- /** Returns the last list of {@link VibrationEffectSegment}. */
- public List<VibrationEffectSegment> getVibrationEffectSegments() {
- return vibrationEffectSegments;
+ /** Returns the last list of {@link PrimitiveSegment} vibrations in {@link PrimitiveEffect}. */
+ @SuppressWarnings("JdkCollectors") // toImmutableList is only supported in Java 8+.
+ public List<PrimitiveEffect> getPrimitiveSegmentsInPrimitiveEffects() {
+ return vibrationEffectSegments.stream()
+ .filter(segment -> segment instanceof PrimitiveSegment)
+ .map(
+ segment ->
+ new PrimitiveEffect(
+ ReflectionHelpers.getField(segment, "mPrimitiveId"),
+ ReflectionHelpers.getField(segment, "mScale"),
+ ReflectionHelpers.getField(segment, "mDelay")))
+ .collect(Collectors.toList());
}
/** Returns the last list of {@link PrimitiveEffect}. */
@@ -108,9 +118,9 @@ public class ShadowVibrator {
supportedPrimitives.addAll(primitives);
}
- /** Returns the {@link VibrationAttributes} from the last vibration. */
+ /** Returns the {@link android.os.VibrationAttributes} from the last vibration. */
@Nullable
- public VibrationAttributes getVibrationAttributesFromLastVibration() {
+ public Object getVibrationAttributesFromLastVibration() {
return vibrationAttributesFromLastVibration;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
index 70cb36999..5d06f587e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
@@ -524,31 +524,29 @@ public class ShadowView {
@Implementation
protected boolean post(Runnable action) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return reflector(_View_.class, realView).post(action);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
return true;
+ } else {
+ return reflector(_View_.class, realView).post(action);
}
}
@Implementation
protected boolean postDelayed(Runnable action, long delayMills) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return reflector(_View_.class, realView).postDelayed(action, delayMills);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowApplication.getInstance()
.getForegroundThreadScheduler()
.postDelayed(action, delayMills);
return true;
+ } else {
+ return reflector(_View_.class, realView).postDelayed(action, delayMills);
}
}
@Implementation
protected void postInvalidateDelayed(long delayMilliseconds) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowApplication.getInstance()
.getForegroundThreadScheduler()
.postDelayed(
@@ -559,17 +557,19 @@ public class ShadowView {
}
},
delayMilliseconds);
+ } else {
+ reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds);
}
}
@Implementation
protected boolean removeCallbacks(Runnable callback) {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- return reflector(_View_.class, realView).removeCallbacks(callback);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowLegacyLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
shadowLooper.getScheduler().remove(callback);
return true;
+ } else {
+ return reflector(_View_.class, realView).removeCallbacks(callback);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
index 28e668067..2f13fcf7d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
@@ -9,7 +9,7 @@ import android.view.ViewGroup;
import java.io.PrintStream;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.reflector.Direct;
@@ -29,10 +29,10 @@ public class ShadowViewGroup extends ShadowView {
() -> {
reflector(ViewGroupReflector.class, realViewGroup).addView(child, index, params);
};
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- addViewRunnable.run();
- } else {
+ if (ShadowLooper.looperMode() == Mode.LEGACY) {
shadowMainLooper().runPaused(addViewRunnable);
+ } else {
+ addViewRunnable.run();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java
new file mode 100644
index 000000000..99f807b07
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java
@@ -0,0 +1,67 @@
+package org.robolectric.shadows;
+
+import android.content.Intent;
+import android.net.PlatformVpnProfile;
+import android.net.VpnManager;
+import android.net.VpnProfileState;
+import android.os.Build.VERSION_CODES;
+import java.util.UUID;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/** Shadow for {@link VpnManager}. */
+@Implements(value = VpnManager.class, minSdk = VERSION_CODES.R)
+public class ShadowVpnManager {
+
+ private VpnProfileState vpnProfileState;
+ private Intent provisionVpnProfileIntent;
+
+ @Implementation
+ protected void deleteProvisionedVpnProfile() {
+ vpnProfileState = null;
+ }
+
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ protected VpnProfileState getProvisionedVpnProfileState() {
+ return vpnProfileState;
+ }
+
+ /**
+ * @see #setProvisionVpnProfileResult(Intent).
+ */
+ @Implementation
+ protected Intent provisionVpnProfile(PlatformVpnProfile profile) {
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+ vpnProfileState = new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, null, false, false);
+ }
+ return provisionVpnProfileIntent;
+ }
+
+ /** Sets the return value of #provisionVpnProfile(PlatformVpnProfile). */
+ public void setProvisionVpnProfileResult(Intent intent) {
+ provisionVpnProfileIntent = intent;
+ }
+
+ @Implementation
+ protected void startProvisionedVpnProfile() {
+ startProvisionedVpnProfileSession();
+ }
+
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ protected String startProvisionedVpnProfileSession() {
+ String sessionKey = UUID.randomUUID().toString();
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+ vpnProfileState =
+ new VpnProfileState(VpnProfileState.STATE_CONNECTED, sessionKey, false, false);
+ }
+ return sessionKey;
+ }
+
+ @Implementation
+ protected void stopProvisionedVpnProfile() {
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+ vpnProfileState = new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, null, false, false);
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
index 706d86e10..1a8131f01 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
@@ -569,7 +569,7 @@ public class ShadowWebView extends ShadowViewGroup {
*
* @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()}
* @deprecated Do not depend on this method as it will be removed in a future update. The
- * preferered method is to populate a fake web history to use for going back.
+ * preferred method is to populate a fake web history to use for going back.
*/
@Deprecated
public void setCanGoBack(boolean canGoBack) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
index 8e933d93e..7221e69e7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
@@ -5,6 +5,9 @@ import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static java.util.stream.Collectors.toList;
import android.content.Context;
import android.content.Intent;
@@ -17,14 +20,19 @@ import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
+import android.net.wifi.WifiSsid;
import android.net.wifi.WifiUsabilityStatsEntry;
+import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.Pair;
import com.google.common.collect.ImmutableList;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.BitSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -71,6 +79,8 @@ public class ShadowWifiManager {
@RealObject WifiManager wifiManager;
private WifiConfiguration apConfig;
private SoftApConfiguration softApConfig;
+ private final Object pnoRequestLock = new Object();
+ private PnoScanRequest outstandingPnoScanRequest = null;
@Implementation
protected boolean setWifiEnabled(boolean wifiEnabled) {
@@ -657,4 +667,176 @@ public class ShadowWifiManager {
this.predictionHorizonSec = predictionHorizonSec;
}
}
+
+ /** Informs the {@link WifiManager} of a list of PNO {@link ScanResult}. */
+ public void networksFoundFromPnoScan(List<ScanResult> scanResults) {
+ synchronized (pnoRequestLock) {
+ List<ScanResult> scanResultsCopy = List.copyOf(scanResults);
+ if (outstandingPnoScanRequest == null
+ || outstandingPnoScanRequest.ssids.stream()
+ .noneMatch(
+ ssid ->
+ scanResultsCopy.stream()
+ .anyMatch(scanResult -> scanResult.getWifiSsid().equals(ssid)))) {
+ return;
+ }
+ Executor executor = outstandingPnoScanRequest.executor;
+ InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback;
+ executor.execute(() -> callback.onScanResultsAvailable(scanResultsCopy));
+ Intent intent = createPnoScanResultsBroadcastIntent();
+ getContext().sendBroadcast(intent);
+ executor.execute(
+ () ->
+ callback.onRemoved(
+ InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED));
+ outstandingPnoScanRequest = null;
+ }
+ }
+
+ // Object needs to be used here since PnoScanResultsCallback is hidden. The looseSignatures spec
+ // requires that all args are of type Object.
+ @Implementation(minSdk = TIRAMISU)
+ @HiddenApi
+ protected void setExternalPnoScanRequest(
+ Object ssids, Object frequencies, Object executor, Object callback) {
+ synchronized (pnoRequestLock) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+
+ List<WifiSsid> pnoSsids = (List<WifiSsid>) ssids;
+ int[] pnoFrequencies = (int[]) frequencies;
+ Executor pnoExecutor = (Executor) executor;
+ InternalPnoScanResultsCallback pnoCallback = new InternalPnoScanResultsCallback(callback);
+
+ if (pnoExecutor == null) {
+ throw new IllegalArgumentException("executor cannot be null");
+ }
+ if (pnoSsids == null || pnoSsids.isEmpty()) {
+ // The real WifiServiceImpl throws an IllegalStateException in this case, so keeping it the
+ // same for consistency.
+ throw new IllegalStateException("Ssids can't be null or empty");
+ }
+ if (pnoSsids.size() > 2) {
+ throw new IllegalArgumentException("Ssid list can't be greater than 2");
+ }
+ if (pnoFrequencies != null && pnoFrequencies.length > 10) {
+ throw new IllegalArgumentException("Length of frequencies must be smaller than 10");
+ }
+ int uid = Binder.getCallingUid();
+ String packageName = getContext().getPackageName();
+
+ if (outstandingPnoScanRequest != null) {
+ pnoExecutor.execute(
+ () ->
+ pnoCallback.onRegisterFailed(
+ uid == outstandingPnoScanRequest.uid
+ ? InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED
+ : InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY));
+ return;
+ }
+
+ outstandingPnoScanRequest =
+ new PnoScanRequest(pnoSsids, pnoFrequencies, pnoExecutor, pnoCallback, packageName, uid);
+ pnoExecutor.execute(pnoCallback::onRegisterSuccess);
+ }
+ }
+
+ @Implementation(minSdk = TIRAMISU)
+ @HiddenApi
+ protected void clearExternalPnoScanRequest() {
+ synchronized (pnoRequestLock) {
+ if (outstandingPnoScanRequest != null
+ && outstandingPnoScanRequest.uid == Binder.getCallingUid()) {
+ InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback;
+ outstandingPnoScanRequest.executor.execute(
+ () ->
+ callback.onRemoved(
+ InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED));
+ outstandingPnoScanRequest = null;
+ }
+ }
+ }
+
+ private static class PnoScanRequest {
+ private final List<WifiSsid> ssids;
+ private final List<Integer> frequencies;
+ private final Executor executor;
+ private final InternalPnoScanResultsCallback callback;
+ private final String packageName;
+ private final int uid;
+
+ private PnoScanRequest(
+ List<WifiSsid> ssids,
+ int[] frequencies,
+ Executor executor,
+ InternalPnoScanResultsCallback callback,
+ String packageName,
+ int uid) {
+ this.ssids = List.copyOf(ssids);
+ this.frequencies =
+ frequencies == null ? List.of() : Arrays.stream(frequencies).boxed().collect(toList());
+ this.executor = executor;
+ this.callback = callback;
+ this.packageName = packageName;
+ this.uid = uid;
+ }
+ }
+
+ private Intent createPnoScanResultsBroadcastIntent() {
+ Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+ intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
+ intent.setPackage(outstandingPnoScanRequest.packageName);
+ return intent;
+ }
+
+ private static class InternalPnoScanResultsCallback {
+ static final int REGISTER_PNO_CALLBACK_ALREADY_REGISTERED = 1;
+ static final int REGISTER_PNO_CALLBACK_RESOURCE_BUSY = 2;
+ static final int REMOVE_PNO_CALLBACK_RESULTS_DELIVERED = 1;
+ static final int REMOVE_PNO_CALLBACK_UNREGISTERED = 2;
+
+ final Object callback;
+ final Method availableCallback;
+ final Method successCallback;
+ final Method failedCallback;
+ final Method removedCallback;
+
+ InternalPnoScanResultsCallback(Object callback) {
+ this.callback = callback;
+ try {
+ Class<?> pnoCallbackClass = callback.getClass();
+ availableCallback = pnoCallbackClass.getMethod("onScanResultsAvailable", List.class);
+ successCallback = pnoCallbackClass.getMethod("onRegisterSuccess");
+ failedCallback = pnoCallbackClass.getMethod("onRegisterFailed", int.class);
+ removedCallback = pnoCallbackClass.getMethod("onRemoved", int.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("callback is not of type PnoScanResultsCallback", e);
+ }
+ }
+
+ void onScanResultsAvailable(List<ScanResult> scanResults) {
+ invokeCallback(availableCallback, scanResults);
+ }
+
+ void onRegisterSuccess() {
+ invokeCallback(successCallback);
+ }
+
+ void onRegisterFailed(int reason) {
+ invokeCallback(failedCallback, reason);
+ }
+
+ void onRemoved(int reason) {
+ invokeCallback(removedCallback, reason);
+ }
+
+ void invokeCallback(Method method, Object... args) {
+ try {
+ method.invoke(callback, args);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new IllegalStateException("Failed to invoke " + method.getName(), e);
+ }
+ }
+ }
}