aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRam Peri <ramperi@google.com>2023-06-20 18:59:11 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-06-20 18:59:11 +0000
commit95ace1ade5a74c7df4250b762af7c9a242c7f50d (patch)
tree47e18158a12b86676f2b5e54a1f7b98764a73509
parent632a3a0f4ecf3f7fb5453b4444f62fd1a67cbd85 (diff)
parenta99b56cbdb00d3f2673b19d5d729912686a14ef5 (diff)
downloadrobolectric-95ace1ade5a74c7df4250b762af7c9a242c7f50d.tar.gz
Merge branch 'upstream-google' into rng_import18 am: a99b56cbdb
Original change: https://googleplex-android-review.googlesource.com/c/platform/external/robolectric/+/23691011 Change-Id: Icbbcda56ac31a543a9c9496ad0eaee5a90f1272d Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java18
-rw-r--r--nativeruntime/build.gradle2
-rw-r--r--processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java5
-rw-r--r--processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java18
-rw-r--r--processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java26
-rw-r--r--processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java15
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java2
-rw-r--r--robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java8
-rw-r--r--robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java12
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java114
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java81
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java95
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java50
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java186
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java365
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java109
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java39
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java24
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java72
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java42
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java33
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java63
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java95
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java322
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java23
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java117
-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/CellInfoLteBuilder.java139
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java96
-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--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/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.java108
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java21
-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/ShadowLegacyLooper.java2
-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/ShadowPausedLooper.java99
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java69
-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.java11
-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/ShadowTelephonyManager.java39
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java20
-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/ShadowWifiManager.java182
-rw-r--r--utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java11
-rw-r--r--utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java14
-rw-r--r--utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java199
-rw-r--r--utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java41
72 files changed, 4103 insertions, 241 deletions
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
index fa11ce7e2..eea5deaee 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
@@ -12,6 +12,7 @@ import android.database.sqlite.SQLiteException;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.Suppress;
import com.google.common.base.Ascii;
import com.google.common.base.Throwables;
import com.google.common.io.ByteStreams;
@@ -174,7 +175,8 @@ public class SQLiteDatabaseTest {
}
// TODO(hoisie): This test crashes in emulators, enable when it is fixed in Android.
- @SdkSuppress(minSdkVersion = 34)
+ // Use Suppress here to stop it from running on emulators, but not on Robolectric
+ @Suppress
@Test
public void cursorWindow_finalize_concurrentStressTest() throws Throwable {
final PrintStream originalErr = System.err;
@@ -223,4 +225,18 @@ public class SQLiteDatabaseTest {
c.close();
assertThat(sorted).containsExactly("aaa", "abc", "ABC", "bbb").inOrder();
}
+
+ @Test
+ @Config(minSdk = LOLLIPOP)
+ @SdkSuppress(minSdkVersion = LOLLIPOP)
+ public void regex_selection() {
+ ContentValues values = new ContentValues();
+ values.put("first_column", "test");
+ database.insert("table_name", null, values);
+ String select = "first_column regexp ?";
+ String[] selectArgs = {
+ "test",
+ };
+ assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1);
+ }
}
diff --git a/nativeruntime/build.gradle b/nativeruntime/build.gradle
index 1ef93175a..f8d496d5b 100644
--- a/nativeruntime/build.gradle
+++ b/nativeruntime/build.gradle
@@ -66,7 +66,7 @@ dependencies {
api project(":utils:reflector")
api "com.google.guava:guava:$guavaJREVersion"
- implementation "org.robolectric:nativeruntime-dist-compat:1.0.0"
+ implementation "org.robolectric:nativeruntime-dist-compat:1.0.1"
annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
diff --git a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
index fd5e77dd2..d9905f6cf 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
@@ -1,5 +1,6 @@
package org.robolectric.annotation.processing;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.newTreeMap;
import static com.google.common.collect.Sets.newTreeSet;
@@ -127,6 +128,10 @@ public class RobolectricModel {
}
public void addResetter(TypeElement shadowTypeElement, ExecutableElement elem) {
+ checkState(
+ !resetterMap.containsKey(shadowTypeElement.getQualifiedName().toString()),
+ "Trying to register a duplicate resetter on %s",
+ shadowTypeElement.getQualifiedName());
registerType(shadowTypeElement);
resetterMap.put(shadowTypeElement.getQualifiedName().toString(),
diff --git a/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java b/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
index d409f8396..39df257f9 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
@@ -1,6 +1,9 @@
package org.robolectric.annotation.processing.validator;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
@@ -13,6 +16,9 @@ import org.robolectric.annotation.processing.RobolectricModel;
* Validator that checks usages of {@link org.robolectric.annotation.Resetter}.
*/
public class ResetterValidator extends FoundOnImplementsValidator {
+
+ private final Map<TypeElement, ExecutableElement> resetterMethodsByClass = new HashMap<>();
+
public ResetterValidator(RobolectricModel.Builder modelBuilder, ProcessingEnvironment env) {
super(modelBuilder, env, "org.robolectric.annotation.Resetter");
}
@@ -35,7 +41,19 @@ public class ResetterValidator extends FoundOnImplementsValidator {
error("@Resetter methods must not have parameters");
error = true;
}
+ if (resetterMethodsByClass.containsKey(parent)) {
+ error(
+ String.format(
+ Locale.US,
+ "Duplicate @Resetter methods found on %s: %s() and %s(). Only one @Resetter method"
+ + " is permitted on each shadow.",
+ parent.getQualifiedName(),
+ resetterMethodsByClass.get(parent).getSimpleName(),
+ elem.getSimpleName()));
+ error = true;
+ }
if (!error) {
+ resetterMethodsByClass.put(parent, elem);
modelBuilder.addResetter(parent, elem);
}
}
diff --git a/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java b/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
index 68900409b..c7924e8b0 100644
--- a/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
+++ b/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
@@ -12,7 +12,8 @@ import org.junit.runners.JUnit4;
public class ResetterValidatorTest {
@Test
public void resetterWithoutImplements_shouldNotCompile() {
- final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterWithoutImplements";
+ final String testClass =
+ "org.robolectric.annotation.processing.shadows.ShadowResetterWithoutImplements";
assertAbout(singleClass())
.that(testClass)
.failsToCompile()
@@ -22,7 +23,8 @@ public class ResetterValidatorTest {
@Test
public void nonStaticResetter_shouldNotCompile() {
- final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterNonStatic";
+ final String testClass =
+ "org.robolectric.annotation.processing.shadows.ShadowResetterNonStatic";
assertAbout(singleClass())
.that(testClass)
.failsToCompile()
@@ -32,7 +34,8 @@ public class ResetterValidatorTest {
@Test
public void nonPublicResetter_shouldNotCompile() {
- final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterNonPublic";
+ final String testClass =
+ "org.robolectric.annotation.processing.shadows.ShadowResetterNonPublic";
assertAbout(singleClass())
.that(testClass)
.failsToCompile()
@@ -42,7 +45,8 @@ public class ResetterValidatorTest {
@Test
public void resetterWithParameters_shouldNotCompile() {
- final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterWithParameters";
+ final String testClass =
+ "org.robolectric.annotation.processing.shadows.ShadowResetterWithParameters";
assertAbout(singleClass())
.that(testClass)
.failsToCompile()
@@ -51,6 +55,20 @@ public class ResetterValidatorTest {
}
@Test
+ public void twoValidResetters_shouldNotCompile() {
+ final String testClass = "org.robolectric.annotation.processing.shadows.ShadowWithTwoResetters";
+
+ assertAbout(singleClass())
+ .that(testClass)
+ .failsToCompile()
+ .withErrorContaining(
+ "Duplicate @Resetter methods found on"
+ + " org.robolectric.annotation.processing.shadows.ShadowWithTwoResetters:"
+ + " resetter_method_one() and resetter_method_two().")
+ .onLine(13);
+ }
+
+ @Test
public void goodResetter_shouldCompile() {
final String testClass = "org.robolectric.annotation.processing.shadows.ShadowDummy";
assertAbout(singleClass())
diff --git a/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java
new file mode 100644
index 000000000..8183073b6
--- /dev/null
+++ b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java
@@ -0,0 +1,15 @@
+package org.robolectric.annotation.processing.shadows;
+
+import com.example.objects.Dummy;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+@Implements(Dummy.class)
+public class ShadowWithTwoResetters {
+
+ @Resetter
+ public static void resetter_method_one() {}
+
+ @Resetter
+ public static void resetter_method_two() {}
+}
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
index a6908af09..dd1bc5cca 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
@@ -157,7 +157,7 @@ public class LocalUiController implements UiController {
@Override
public void loopMainThreadUntilIdle() {
- if (!ShadowLooper.looperMode().equals(LooperMode.Mode.PAUSED)) {
+ if (ShadowLooper.looperMode().equals(LooperMode.Mode.LEGACY)) {
shadowMainLooper().idle();
} else {
ImmutableSet<IdlingResourceProxy> idlingResources = syncIdlingResources();
diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
index 0ba55d893..c0108de97 100644
--- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
+++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
@@ -50,6 +50,7 @@ import org.robolectric.annotation.Config;
import org.robolectric.annotation.Config.Implementation;
import org.robolectric.annotation.experimental.LazyApplication;
import org.robolectric.annotation.experimental.LazyApplication.LazyLoad;
+import org.robolectric.config.ConfigurationRegistry;
import org.robolectric.internal.AndroidSandbox.TestEnvironmentSpec;
import org.robolectric.internal.ResourcesMode;
import org.robolectric.internal.ShadowProvider;
@@ -163,10 +164,10 @@ public class RobolectricTestRunnerTest {
assertThat(events)
.containsExactly(
"started: first",
- "failure: ShadowActivityThread.reset: ActivityThread not set",
+ "failure: fake error in setUpApplicationState",
"finished: first",
"started: second",
- "failure: ShadowActivityThread.reset: ActivityThread not set",
+ "failure: fake error in setUpApplicationState",
"finished: second")
.inOrder();
}
@@ -319,6 +320,9 @@ public class RobolectricTestRunnerTest {
@Override
public void setUpApplicationState(Method method,
Configuration configuration, AndroidManifest appManifest) {
+ // ConfigurationRegistry.instance is required for resetters.
+ Config config = configuration.get(Config.class);
+ ConfigurationRegistry.instance = new ConfigurationRegistry(configuration.map());
throw new RuntimeException("fake error in setUpApplicationState");
}
}
diff --git a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
index e3163ccdc..679b456f2 100644
--- a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
+++ b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
@@ -66,10 +66,10 @@ public class AndroidInterceptorsIntegrationTest {
@Test
public void systemNanoTime_shouldReturnShadowClockTime() throws Throwable {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- SystemClock.setCurrentTimeMillis(200);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos());
+ } else {
+ SystemClock.setCurrentTimeMillis(200);
}
long nanoTime = invokeDynamic(System.class, "nanoTime", long.class);
@@ -78,10 +78,10 @@ public class AndroidInterceptorsIntegrationTest {
@Test
public void systemCurrentTimeMillis_shouldReturnShadowClockTime() throws Throwable {
- if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
- SystemClock.setCurrentTimeMillis(200);
- } else {
+ if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos());
+ } else {
+ SystemClock.setCurrentTimeMillis(200);
}
long currentTimeMillis = invokeDynamic(System.class, "currentTimeMillis", long.class);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java
new file mode 100644
index 000000000..72887ddb5
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java
@@ -0,0 +1,114 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellIdentityLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellIdentityLteBuilderTest {
+
+ private static final String MCC = "310";
+ private static final String MNC = "260";
+ private static final int CI = 0;
+ private static final int PCI = 1;
+ private static final int TAC = 2;
+ private static final int EARFCN = 4;
+ private static final int[] BANDS = new int[] {2, 4};
+ private static final int BANDWIDTH = 5;
+ private static final String SHORT_OPERATOR_NAME = "short operator name";
+ private static final String LONG_OPERATOR_NAME = "long operator name";
+ private static final ImmutableList<String> ADDITIONAL_PLMNS = ImmutableList.of("310240");
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellIdentityLte cellIdentity = CellIdentityLteBuilder.newBuilder().build();
+
+ assertThat(cellIdentity.getCi()).isEqualTo(CellInfo.UNAVAILABLE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.M)
+ public void build_sdkJtoM() {
+ CellIdentityLte cellIdentity = getCellIdentityLte();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.N, maxSdk = Build.VERSION_CODES.O_MR1)
+ public void build_sdkNtoO() {
+ CellIdentityLte cellIdentity = getCellIdentityLte();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R)
+ public void build_sdkPtoQ() {
+ CellIdentityLte cellIdentity = getCellIdentityLte();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ assertThat(cellIdentity.getMccString()).isEqualTo(MCC);
+ assertThat(cellIdentity.getMncString()).isEqualTo(MNC);
+ assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+ assertThat(cellIdentity.getBandwidth()).isEqualTo(BANDWIDTH);
+ assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME);
+ assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.S)
+ public void build_fromSdkS() {
+ CellIdentityLte cellIdentity = getCellIdentityLte();
+
+ assertCellIdentityFieldsForAllSdks(cellIdentity);
+ assertThat(cellIdentity.getMccString()).isEqualTo(MCC);
+ assertThat(cellIdentity.getMncString()).isEqualTo(MNC);
+ assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+ assertThat(cellIdentity.getBandwidth()).isEqualTo(BANDWIDTH);
+ assertThat(cellIdentity.getBands()).isEqualTo(BANDS);
+ assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME);
+ assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME);
+ assertThat(cellIdentity.getAdditionalPlmns()).containsExactlyElementsIn(ADDITIONAL_PLMNS);
+ }
+
+ /**
+ * Assertions on {@link android.telephony.CellIdentityLte} values that are common across all
+ * tested SDKs.
+ */
+ private void assertCellIdentityFieldsForAllSdks(CellIdentityLte cellIdentity) {
+ assertThat(cellIdentity.getMcc()).isEqualTo(Integer.parseInt(MCC));
+ assertThat(cellIdentity.getMnc()).isEqualTo(Integer.parseInt(MNC));
+ assertThat(cellIdentity.getCi()).isEqualTo(CI);
+ assertThat(cellIdentity.getPci()).isEqualTo(PCI);
+ assertThat(cellIdentity.getTac()).isEqualTo(TAC);
+ }
+
+ private CellIdentityLte getCellIdentityLte() {
+ return CellIdentityLteBuilder.newBuilder()
+ .setMcc(MCC)
+ .setMnc(MNC)
+ .setCi(CI)
+ .setPci(PCI)
+ .setTac(TAC)
+ .setEarfcn(EARFCN)
+ .setBands(BANDS)
+ .setBandwidth(BANDWIDTH)
+ .setLongOperatorName(LONG_OPERATOR_NAME)
+ .setShortOperatorName(SHORT_OPERATOR_NAME)
+ .setAdditionalPlmns(ADDITIONAL_PLMNS)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java
new file mode 100644
index 000000000..f61abad90
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java
@@ -0,0 +1,81 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfoLte;
+import android.telephony.CellSignalStrengthLte;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.time.Duration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellInfoLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellInfoLteBuilderTest {
+
+ private static final boolean REGISTERED = false;
+ private static final long TIMESTAMP_NANOS = 123L;
+ private static final long TIMESTAMP_MILLIS = Duration.ofNanos(TIMESTAMP_NANOS).toMillis();
+ private static final int CELL_CONNECTION_STATUS = 1;
+
+ private static final CellIdentityLte cellIdentity =
+ CellIdentityLteBuilder.newBuilder().setMcc("310").build();
+ private static final CellSignalStrengthLte cellSignalStrength =
+ CellSignalStrengthLteBuilder.newBuilder().setRsrp(-120).build();
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellInfoLte cellInfo = CellInfoLteBuilder.newBuilder().build();
+
+ assertThat(cellInfo.getTimeStamp()).isEqualTo(0);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.N_MR1)
+ public void build_sdkJtoN() {
+ CellInfoLte cellInfo = getCellInfoLte();
+
+ assertThat(cellInfo.isRegistered()).isFalse();
+ assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS);
+ assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.P, maxSdk = Build.VERSION_CODES.Q)
+ public void build_fromSdkPtoQ() {
+ CellInfoLte cellInfo = getCellInfoLte();
+
+ assertThat(cellInfo.isRegistered()).isFalse();
+ assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS);
+ assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS);
+ assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.R, maxSdk = Config.NEWEST_SDK)
+ public void build_fromSdkR() {
+ CellInfoLte cellInfo = getCellInfoLte();
+
+ assertThat(cellInfo.isRegistered()).isFalse();
+ assertThat(cellInfo.getTimestampMillis()).isEqualTo(TIMESTAMP_MILLIS);
+ assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS);
+ assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+ assertThat(cellInfo.getCellIdentity()).isEqualTo(cellIdentity);
+ }
+
+ private CellInfoLte getCellInfoLte() {
+ return CellInfoLteBuilder.newBuilder()
+ .setRegistered(REGISTERED)
+ .setTimeStampNanos(TIMESTAMP_NANOS)
+ .setCellConnectionStatus(CELL_CONNECTION_STATUS)
+ .setCellIdentity(cellIdentity)
+ .setCellSignalStrength(cellSignalStrength)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java
new file mode 100644
index 000000000..cfd3bbeeb
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java
@@ -0,0 +1,95 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthLte;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellSignalStrengthLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellSignalStrengthLteBuilderTest {
+
+ // The platform enforces that some of these values are within a certain range - otherwise, it will
+ // default to {@link android.telephony.CellInfo.UNAVAILABLE}.
+ private static final int RSSI = -100;
+ private static final int RSRP = -120;
+ private static final int RSRQ = -10;
+ private static final int RSSNR = 30;
+ private static final int CQI_TABLE_INDEX = 4;
+ private static final int CQI = 5;
+ private static final int TIMING_ADVANCE = 6;
+
+ @Test
+ public void build_noArguments() {
+ // The intent is to primarily verify that there are no issues setting default values i.e., no
+ // exceptions thrown or invalid inputs.
+ CellSignalStrengthLte cellSignalStrength = CellSignalStrengthLteBuilder.newBuilder().build();
+
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(CellInfo.UNAVAILABLE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.N_MR1)
+ public void build_sdkJtoN() {
+ CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.O, maxSdk = Build.VERSION_CODES.P)
+ public void build_sdkOToP() {
+ CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+ assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+ assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+ assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+ assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R)
+ public void build_sdkQtoR() {
+ CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+ assertThat(cellSignalStrength.getRssi()).isEqualTo(RSSI);
+ assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+ assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+ assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+ assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.S)
+ public void build_fromSdkS() {
+ CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+ assertThat(cellSignalStrength.getRssi()).isEqualTo(RSSI);
+ assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+ assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+ assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+ assertThat(cellSignalStrength.getCqiTableIndex()).isEqualTo(CQI_TABLE_INDEX);
+ assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+ assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+ }
+
+ private CellSignalStrengthLte getCellSignalStrength() {
+ return CellSignalStrengthLteBuilder.newBuilder()
+ .setRssi(RSSI)
+ .setRsrp(RSRP)
+ .setRsrq(RSRQ)
+ .setRssnr(RSSNR)
+ .setCqi(CQI)
+ .setCqiTableIndex(CQI_TABLE_INDEX)
+ .setTimingAdvance(TIMING_ADVANCE)
+ .build();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
index 27e635c8f..06769c66e 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
@@ -14,6 +14,7 @@ import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaCodecList;
import android.media.MediaFormat;
+import android.util.Range;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -29,6 +30,10 @@ public class MediaCodecInfoBuilderTest {
private static final String VP9_DECODER_NAME = "test.decoder.vp9";
private static final String MULTIFORMAT_ENCODER_NAME = "test.encoder.multiformat";
+ private static final int WIDTH = 1920;
+ private static final int HEIGHT = 1080;
+ private static final Range<Integer> DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE = new Range<>(2, 896);
+
private static final MediaFormat AAC_MEDIA_FORMAT =
createMediaFormat(
MIMETYPE_AUDIO_AAC, new String[] {CodecCapabilities.FEATURE_DynamicTimestamp});
@@ -37,6 +42,9 @@ public class MediaCodecInfoBuilderTest {
MIMETYPE_AUDIO_OPUS, new String[] {CodecCapabilities.FEATURE_AdaptivePlayback});
private static final MediaFormat AVC_MEDIA_FORMAT =
createMediaFormat(MIMETYPE_VIDEO_AVC, new String[] {CodecCapabilities.FEATURE_IntraRefresh});
+ private static final MediaFormat AVC_MEDIA_FORMAT_WITH_RESOLUTION =
+ createMediaFormat(
+ MIMETYPE_VIDEO_AVC, WIDTH, HEIGHT, new String[] {CodecCapabilities.FEATURE_IntraRefresh});
private static final MediaFormat VP9_MEDIA_FORMAT =
createMediaFormat(
MIMETYPE_VIDEO_VP9,
@@ -123,6 +131,10 @@ public class MediaCodecInfoBuilderTest {
assertThat(codecCapabilities.getMimeType()).isEqualTo(MIMETYPE_VIDEO_AVC);
assertThat(codecCapabilities.getAudioCapabilities()).isNull();
assertThat(codecCapabilities.getVideoCapabilities()).isNotNull();
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths())
+ .isEqualTo(DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE);
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights())
+ .isEqualTo(DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE);
assertThat(codecCapabilities.getEncoderCapabilities()).isNotNull();
assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_IntraRefresh))
.isTrue();
@@ -136,6 +148,24 @@ public class MediaCodecInfoBuilderTest {
@Test
@Config(minSdk = Q)
+ public void canCreateVideoEncoderCapabilities_supportedFormatResolutionIsSet() {
+ CodecCapabilities codecCapabilities =
+ MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
+ .setMediaFormat(AVC_MEDIA_FORMAT_WITH_RESOLUTION)
+ .setIsEncoder(true)
+ .setProfileLevels(AVC_PROFILE_LEVELS)
+ .setColorFormats(AVC_COLOR_FORMATS)
+ .build();
+
+ assertThat(codecCapabilities.getVideoCapabilities()).isNotNull();
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths())
+ .isEqualTo(new Range<>(1, WIDTH));
+ assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights())
+ .isEqualTo(new Range<>(1, HEIGHT));
+ }
+
+ @Test
+ @Config(minSdk = Q)
public void canCreateVideoDecoderCapabilities() {
CodecCapabilities codecCapabilities =
MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
@@ -353,4 +383,24 @@ public class MediaCodecInfoBuilderTest {
}
return mediaFormat;
}
+
+ /**
+ * Create a sample {@link MediaFormat}.
+ *
+ * @param mime one of MIMETYPE_* from {@link MediaFormat}.
+ * @param width The width of the content (in pixels).
+ * @param height The height of the content (in pixels).
+ * @param features an array of CodecCapabilities.FEATURE_ features to be enabled.
+ */
+ private static MediaFormat createMediaFormat(
+ String mime, int width, int height, String[] features) {
+ MediaFormat mediaFormat = new MediaFormat();
+ mediaFormat.setString(MediaFormat.KEY_MIME, mime);
+ mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
+ mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
+ for (String feature : features) {
+ mediaFormat.setFeatureEnabled(feature, true);
+ }
+ return mediaFormat;
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
index b798a74a3..d5c0c46ca 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
@@ -8,6 +8,7 @@ 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.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -24,6 +25,7 @@ import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioRecordingConfiguration;
+import android.media.AudioSystem;
import android.media.MediaRecorder.AudioSource;
import android.media.audiopolicy.AudioPolicy;
import androidx.test.core.app.ApplicationProvider;
@@ -991,6 +993,190 @@ public class ShadowAudioManagerTest {
assertThat(audioSessionId).isNotEqualTo(audioSessionId2);
}
+ @Test
+ @Config(minSdk = Q)
+ public void isOffloadSupported_withoutSupport() {
+ assertThat(
+ AudioManager.isOffloadedPlaybackSupported(
+ new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build(),
+ new AudioAttributes.Builder().build()))
+ .isFalse();
+ }
+
+ @Test
+ @Config(minSdk = Q, maxSdk = R)
+ public void isOffloadSupported_withSetOffloadSupported() {
+ AudioFormat format =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+
+ ShadowAudioSystem.setOffloadSupported(format, attributes, true);
+
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Q, maxSdk = R)
+ public void isOffloadSupported_withSetOffloadSupportedAddedAndRemoved() {
+ AudioFormat format =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setOffloadSupported(format, attributes, true);
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+
+ ShadowAudioSystem.setOffloadSupported(format, attributes, false);
+
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void isOffloadSupported_withSetOffloadPlaybackSupport() {
+ AudioFormat format =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+
+ ShadowAudioSystem.setOffloadPlaybackSupport(format, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void getPlaybackOffloadSupport_withSetOffloadSupport_returnsOffloadSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ ShadowAudioSystem.setOffloadPlaybackSupport(
+ audioFormat, audioAttributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ int playbackOffloadSupport =
+ AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_SUPPORTED);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void
+ getPlaybackOffloadSupport_withoutSetDirectPlaybackSupport_returnsOffloadNotSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+
+ int playbackOffloadSupport =
+ AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_NOT_SUPPORTED);
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void getPlaybackOffloadSupport_withSameAudioAttrUsage_returnsOffloadSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ ShadowAudioSystem.setOffloadPlaybackSupport(
+ audioFormat, audioAttributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ AudioAttributes audioAttributes2 =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ int playbackOffloadSupport =
+ AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes2);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_SUPPORTED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getDirectPlaybackSupport_withSetDirectPlaybackSupport_returnsOffloadSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ ShadowAudioSystem.setDirectPlaybackSupport(
+ audioFormat, audioAttributes, AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+
+ int playbackOffloadSupport =
+ AudioManager.getDirectPlaybackSupport(audioFormat, audioAttributes);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getDirectPlaybackSupport_withShadowAudioSystemReset_returnsOffloadNotSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setSampleRate(48_000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+ .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ ShadowAudioSystem.setDirectPlaybackSupport(
+ audioFormat, audioAttributes, AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+ ShadowAudioSystem.reset();
+
+ int playbackOffloadSupport =
+ AudioManager.getDirectPlaybackSupport(audioFormat, audioAttributes);
+
+ assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_NOT_SUPPORTED);
+ }
+
private static AudioDeviceInfo createAudioDevice(int type) throws ReflectiveOperationException {
AudioDeviceInfo info = Shadow.newInstanceOf(AudioDeviceInfo.class);
Field portField = AudioDeviceInfo.class.getDeclaredField("mPort");
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
index adffa0111..e8869bd49 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
@@ -4,13 +4,20 @@ import static android.media.AudioTrack.ERROR_BAD_VALUE;
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.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.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
+import android.media.AudioSystem;
import android.media.AudioTrack;
+import android.media.PlaybackParams;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.nio.ByteBuffer;
import org.junit.Test;
@@ -172,6 +179,360 @@ public class ShadowAudioTrackTest implements ShadowAudioTrack.OnAudioDataWritten
assertThat(written).isEqualTo(ERROR_BAD_VALUE);
}
+ @Test
+ @Config(minSdk = M)
+ public void getPlaybackParams_withSetPlaybackParams_returnsSetPlaybackParams() {
+ PlaybackParams playbackParams =
+ new PlaybackParams()
+ .allowDefaults()
+ .setSpeed(1.0f)
+ .setPitch(1.0f)
+ .setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_FAIL);
+ AudioTrack audioTrack = getSampleAudioTrack();
+ audioTrack.setPlaybackParams(playbackParams);
+
+ assertThat(audioTrack.getPlaybackParams()).isEqualTo(playbackParams);
+ }
+
+ @Test
+ public void addDirectPlaybackSupport_forPcmEncoding_throws() {
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_8BIT), attributes));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_16BIT), attributes));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_24BIT_PACKED), attributes));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_32BIT), attributes));
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ShadowAudioTrack.addDirectPlaybackSupport(
+ getAudioFormat(AudioFormat.ENCODING_PCM_FLOAT), attributes));
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void isDirectPlaybackSupported() {
+ AudioFormat ac3Format = getAudioFormat(AudioFormat.ENCODING_AC3);
+ AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+
+ assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isFalse();
+
+ ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+
+ assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void isDirectPlaybackSupported_differentFormatOrAttributeFields() {
+ AudioFormat ac3Format = new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build();
+ AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+
+ ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+
+ assertThat(
+ AudioTrack.isDirectPlaybackSupported(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(65000)
+ .build(),
+ audioAttributes))
+ .isFalse();
+ assertThat(
+ AudioTrack.isDirectPlaybackSupported(
+ ac3Format,
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+ .build()))
+ .isFalse();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void clearDirectPlaybackSupportedEncodings() {
+ AudioFormat ac3Format = new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build();
+ AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+ ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+ assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isTrue();
+
+ ShadowAudioTrack.clearDirectPlaybackSupportedFormats();
+
+ assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isFalse();
+ }
+
+ @Test
+ public void addAllowedNonPcmEncoding_forPcmEncoding_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_8BIT));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_16BIT));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_24BIT_PACKED));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_32BIT));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_FLOAT));
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void createInstance_withNonPcmEncodingNotAllowed_throws() {
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setBufferSizeInBytes(65536)
+ .build());
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void createInstance_withNonPcmEncodingAllowed() {
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setBufferSizeInBytes(65536)
+ .build();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void createInstance_withOffloadAndEncodingNotOffloaded_throws() {
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setBufferSizeInBytes(65536)
+ .setOffloadedPlayback(true)
+ .build());
+ }
+
+ @Test
+ @Config(minSdk = Q, maxSdk = R)
+ public void createInstance_withOffloadAndEncodingIsOffloadSupported() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setOffloadSupported(audioFormat, attributes, /* supported= */ true);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(audioFormat)
+ .setAudioAttributes(attributes)
+ .setBufferSizeInBytes(65536)
+ .setOffloadedPlayback(true)
+ .build();
+
+ assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+ }
+
+ @Test
+ @Config(sdk = S)
+ public void createInstance_withOffloadAndGetOffloadSupport() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setOffloadPlaybackSupport(
+ audioFormat, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(audioFormat)
+ .setAudioAttributes(attributes)
+ .setBufferSizeInBytes(65536)
+ .setOffloadedPlayback(true)
+ .build();
+
+ assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void createInstance_withOffloadAndGetDirectPlaybackSupport() {
+ AudioFormat audioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setDirectPlaybackSupport(
+ audioFormat, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(audioFormat)
+ .setAudioAttributes(attributes)
+ .setBufferSizeInBytes(65536)
+ .setOffloadedPlayback(true)
+ .build();
+
+ assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void clearAllowedNonPcmEncodings() {
+ AudioFormat surroundAudioFormat =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+ new AudioTrack.Builder()
+ .setAudioFormat(surroundAudioFormat)
+ .setBufferSizeInBytes(65536)
+ .build();
+
+ ShadowAudioTrack.clearAllowedNonPcmEncodings();
+
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ new AudioTrack.Builder()
+ .setAudioFormat(surroundAudioFormat)
+ .setBufferSizeInBytes(65536)
+ .build());
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void write_withNonPcmEncodingSupported_succeeds() {
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setAudioAttributes(new AudioAttributes.Builder().build())
+ .setBufferSizeInBytes(32 * 1024)
+ .build();
+
+ assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(128);
+ assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)).isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(128);
+ }
+
+ @Test
+ @Config(minSdk = Q, maxSdk = R)
+ public void write_withOffloadUntilApi30_succeeds() {
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+ AudioFormat ac3Format =
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build();
+ AudioAttributes attributes = new AudioAttributes.Builder().build();
+ ShadowAudioSystem.setOffloadSupported(ac3Format, attributes, /* supported= */ true);
+
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(ac3Format)
+ .setAudioAttributes(new AudioAttributes.Builder().build())
+ .setBufferSizeInBytes(32 * 1024)
+ .setOffloadedPlayback(true)
+ .build();
+
+ assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(128);
+ assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)).isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(128);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(128);
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void write_withNonPcmEncodingNoLongerSupported_returnsErrorDeadObject() {
+ ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+ AudioTrack audioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(
+ new AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_AC3)
+ .setSampleRate(48000)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+ .build())
+ .setAudioAttributes(new AudioAttributes.Builder().build())
+ .setBufferSizeInBytes(32 * 1024)
+ .build();
+
+ ShadowAudioTrack.clearAllowedNonPcmEncodings();
+
+ assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+ .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+ .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+ }
+
@Override
@Config(minSdk = Q)
public void onAudioDataWritten(
@@ -195,4 +556,8 @@ public class ShadowAudioTrackTest implements ShadowAudioTrack.OnAudioDataWritten
.build())
.build();
}
+
+ private AudioFormat getAudioFormat(int encoding) {
+ return new AudioFormat.Builder().setEncoding(encoding).build();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
index caed17812..78b9edbd9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
@@ -31,6 +31,7 @@ public class ShadowBluetoothGattTest {
private static final String ACTION_DISCOVER = "DISCOVER";
private static final String ACTION_READ = "READ";
private static final String ACTION_WRITE = "WRITE";
+ private static final String REMOTE_ADDRESS = "R-A";
private int resultStatus = INITIAL_VALUE;
private int resultState = INITIAL_VALUE;
@@ -274,6 +275,15 @@ public class ShadowBluetoothGattTest {
@Test
@Config(minSdk = O)
+ public void getService_afterAddService() {
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ assertThat(bluetoothGatt.discoverServices()).isFalse();
+ assertThat(bluetoothGatt.getService(service1.getUuid())).isEqualTo(service1);
+ assertThat(bluetoothGatt.getService(service2.getUuid())).isNull();
+ }
+
+ @Test
+ @Config(minSdk = O)
public void discoverServices_clearsService() {
shadowOf(bluetoothGatt).setGattCallback(callback);
shadowOf(bluetoothGatt).addDiscoverableService(service1);
@@ -471,4 +481,103 @@ public class ShadowBluetoothGattTest {
assertThat(resultCharacteristic).isEqualTo(characteristic);
assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
}
+
+ @Test
+ public void test_getBluetoothConnectionManager() {
+ assertThat(shadowOf(bluetoothGatt).getBluetoothConnectionManager()).isNotNull();
+ }
+
+ @Test
+ public void test_notifyConnection_connects() {
+ shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue();
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void test_notifyConnection_connectsWithCallbackSet() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue();
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ public void test_notifyDisconnection_disconnects() {
+ shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void test_notifyDisconnection_disconnectsWithCallbackSet() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void test_notifyDisconnection_disconnectsWithCallbackSet_connectedInitially() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+ shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+ assertThat(
+ shadowOf(bluetoothGatt)
+ .getBluetoothConnectionManager()
+ .hasGattClientConnection(REMOTE_ADDRESS))
+ .isFalse();
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void allowCharacteristicNotification_canSetNotification() {
+ service1.addCharacteristic(characteristicWithReadProperty);
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ shadowOf(bluetoothGatt).allowCharacteristicNotification(characteristicWithReadProperty);
+ assertThat(bluetoothGatt.setCharacteristicNotification(characteristicWithReadProperty, true))
+ .isTrue();
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void disallowCharacteristicNotification_cannotSetNotification() {
+ service1.addCharacteristic(characteristicWithReadProperty);
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ shadowOf(bluetoothGatt).disallowCharacteristicNotification(characteristicWithReadProperty);
+ assertThat(bluetoothGatt.setCharacteristicNotification(characteristicWithReadProperty, true))
+ .isFalse();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
index 9482ba8d7..cae11d9fa 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
@@ -22,9 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
@@ -37,8 +35,6 @@ public class ShadowBluetoothHeadsetTest {
private BluetoothHeadset bluetoothHeadset;
private Application context;
- @Rule public ExpectedException thrown = ExpectedException.none();
-
@Before
public void setUp() throws Exception {
device1 = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:AA:BB");
@@ -61,6 +57,41 @@ public class ShadowBluetoothHeadsetTest {
}
@Test
+ public void getConnectedDevices_doesNotReturnDevicesInNonConnectedStates() {
+ shadowOf(bluetoothHeadset).addDevice(device1, BluetoothProfile.STATE_CONNECTING);
+ shadowOf(bluetoothHeadset).addDevice(device2, BluetoothProfile.STATE_DISCONNECTING);
+
+ assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+ }
+
+ @Test
+ public void getConnectionState_returnsStoredConnectionState() {
+ shadowOf(bluetoothHeadset).addDevice(device1, BluetoothProfile.STATE_CONNECTING);
+ shadowOf(bluetoothHeadset).addDevice(device2, BluetoothProfile.STATE_DISCONNECTING);
+
+ assertThat(bluetoothHeadset.getConnectionState(device1))
+ .isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ assertThat(bluetoothHeadset.getConnectionState(device2))
+ .isEqualTo(BluetoothProfile.STATE_DISCONNECTING);
+ }
+
+ @Test
+ public void removeDevice_getConnectionStateReturnsDisconnected() {
+ shadowOf(bluetoothHeadset).addConnectedDevice(device1);
+ shadowOf(bluetoothHeadset).removeDevice(device1);
+
+ assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+ }
+
+ @Test
+ public void removeDevice_getConnectedDevicesReturnsEmpty() {
+ shadowOf(bluetoothHeadset).addConnectedDevice(device1);
+ shadowOf(bluetoothHeadset).removeDevice(device1);
+
+ assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+ }
+
+ @Test
public void getConnectionState_defaultsToDisconnected() {
shadowOf(bluetoothHeadset).addConnectedDevice(device1);
shadowOf(bluetoothHeadset).addConnectedDevice(device2);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
index 8ee669ae1..8000cc656 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
@@ -1,10 +1,14 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.hardware.input.InputManager;
+import android.hardware.input.InputManager.InputDeviceListener;
+import android.os.Handler;
+import android.os.Looper;
import android.view.MotionEvent;
import android.view.VerifiedMotionEvent;
import androidx.test.core.app.ApplicationProvider;
@@ -16,7 +20,7 @@ import org.robolectric.annotation.Config;
/** Unit tests for {@link ShadowInputManager}. */
@RunWith(AndroidJUnit4.class)
-@Config(minSdk = R)
+@Config(minSdk = R, maxSdk = TIRAMISU)
public class ShadowInputManagerTest {
private InputManager inputManager;
@@ -38,4 +42,22 @@ public class ShadowInputManagerTest {
assertThat(verifiedMotionEvent.getEventTimeNanos()).isEqualTo(23456000000L);
assertThat(verifiedMotionEvent.getDownTimeNanos()).isEqualTo(12345000000L);
}
+
+ static class InputDeviceListenerNoOp implements InputDeviceListener {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {}
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {}
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {}
+ }
+
+ @Test
+ public void testRegisterInputDeviceListener_doesNotCrash() {
+ InputDeviceListenerNoOp listener = new InputDeviceListenerNoOp();
+ inputManager.registerInputDeviceListener(listener, new Handler(Looper.getMainLooper()));
+ inputManager.unregisterInputDeviceListener(listener);
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
index cadc3e969..8801f1e1a 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
@@ -4,6 +4,7 @@ import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
@@ -526,10 +527,8 @@ public class ShadowPausedLooperTest {
}
@Test
- public void testIdleNotStuck_whenThreadCrashes() throws Exception {
- HandlerThread thread = new HandlerThread("WillCrash");
- thread.start();
- Looper looper = thread.getLooper();
+ public void idle_looperPaused_idleHandlerThrowsException() throws Exception {
+ Looper looper = handlerThread.getLooper();
shadowOf(looper).pause();
new Handler(looper)
.post(
@@ -537,12 +536,69 @@ public class ShadowPausedLooperTest {
Looper.myQueue()
.addIdleHandler(
() -> {
- throw new RuntimeException();
+ throw new IllegalStateException();
});
});
- shadowOf(looper).idle();
- thread.join(5_000);
- assertThat(thread.getState()).isEqualTo(Thread.State.TERMINATED);
+ assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+ }
+
+ @Test
+ public void idle_looperPaused_runnableThrowsException() throws Exception {
+ Looper looper = handlerThread.getLooper();
+ shadowOf(looper).pause();
+ new Handler(looper)
+ .post(
+ () -> {
+ throw new IllegalStateException();
+ });
+
+ assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+ }
+
+ @Test
+ public void idle_looperRunning_runnableThrowsException() throws Exception {
+ Looper looper = handlerThread.getLooper();
+ new Handler(looper)
+ .post(
+ () -> {
+ throw new IllegalStateException();
+ });
+
+ assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+ }
+
+ @Test
+ public void post_throws_if_looper_died() throws Exception {
+ Looper looper = handlerThread.getLooper();
+ new Handler(looper)
+ .post(
+ () -> {
+ throw new IllegalStateException();
+ });
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+
+ assertThrows(IllegalStateException.class, () -> new Handler(looper).post(() -> {}));
+ }
+
+ @Test
+ public void idle_throws_if_looper_died() throws Exception {
+ Looper looper = handlerThread.getLooper();
+ new Handler(looper)
+ .post(
+ () -> {
+ throw new IllegalStateException();
+ });
+ handlerThread.join(5_000);
+ assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+
+ assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
index 4e7fb16ce..e3b48f0f1 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
@@ -331,6 +331,48 @@ public class ShadowSubscriptionManagerTest {
.isEqualTo("123");
}
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPhoneNumberWithSource_phoneNumberNotSet_returnsEmptyString() {
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_UICC))
+ .isEqualTo("");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER))
+ .isEqualTo("");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_IMS))
+ .isEqualTo("");
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPhoneNumberWithSource_setPhoneNumber_returnsPhoneNumber() {
+ shadowOf(subscriptionManager)
+ .setPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "123");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_UICC))
+ .isEqualTo("123");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER))
+ .isEqualTo("123");
+ assertThat(
+ subscriptionManager.getPhoneNumber(
+ SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+ SubscriptionManager.PHONE_NUMBER_SOURCE_IMS))
+ .isEqualTo("123");
+ }
+
private static class DummySubscriptionsChangedListener
extends SubscriptionManager.OnSubscriptionsChangedListener {
private int subscriptionChangedCount;
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
index 8571627a4..f6b09950d 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
@@ -374,6 +374,21 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ @Config(minSdk = S)
+ public void shouldGiveCallStateForSubscription() {
+ PhoneStateListener listener = mock(PhoneStateListener.class);
+ telephonyManager.listen(listener, LISTEN_CALL_STATE);
+
+ shadowOf(telephonyManager).setCallState(CALL_STATE_RINGING, "911");
+ assertEquals(CALL_STATE_RINGING, telephonyManager.getCallStateForSubscription());
+ verify(listener).onCallStateChanged(CALL_STATE_RINGING, "911");
+
+ shadowOf(telephonyManager).setCallState(CALL_STATE_OFFHOOK, "911");
+ assertEquals(CALL_STATE_OFFHOOK, telephonyManager.getCallStateForSubscription());
+ verify(listener).onCallStateChanged(CALL_STATE_OFFHOOK, null);
+ }
+
+ @Test
public void shouldGiveCallState() {
PhoneStateListener listener = mock(PhoneStateListener.class);
telephonyManager.listen(listener, LISTEN_CALL_STATE);
@@ -803,6 +818,24 @@ public class ShadowTelephonyManagerTest {
}
@Test
+ @Config(minSdk = S)
+ public void setDataEnabledForReasonChangesIsDataEnabledForReason() {
+ int correctReason = TelephonyManager.DATA_ENABLED_REASON_POLICY;
+ int incorrectReason = TelephonyManager.DATA_ENABLED_REASON_USER;
+
+ assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isTrue();
+ assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+
+ telephonyManager.setDataEnabledForReason(correctReason, false);
+ assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isFalse();
+ assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+
+ telephonyManager.setDataEnabledForReason(correctReason, true);
+ assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isTrue();
+ assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+ }
+
+ @Test
public void setDataStateChangesDataState() {
assertThat(telephonyManager.getDataState()).isEqualTo(TelephonyManager.DATA_DISCONNECTED);
shadowOf(telephonyManager).setDataState(TelephonyManager.DATA_CONNECTING);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
index 75df1101a..7c9bfafca 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
@@ -9,6 +9,7 @@ import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
@@ -68,7 +69,8 @@ public class ShadowUserManagerTest {
UserHandle anotherProfile = newUserHandle(2);
shadowOf(userManager).addUserProfile(anotherProfile);
- assertThat(userManager.getUserProfiles()).containsExactly(Process.myUserHandle(), anotherProfile);
+ assertThat(userManager.getUserProfiles())
+ .containsExactly(Process.myUserHandle(), anotherProfile);
}
@Test
@@ -243,7 +245,8 @@ public class ShadowUserManagerTest {
try {
userManager.isManagedProfile();
fail("Expected exception");
- } catch (SecurityException expected) {}
+ } catch (SecurityException expected) {
+ }
setPermissions(permission.MANAGE_USERS);
@@ -317,6 +320,19 @@ public class ShadowUserManagerTest {
}
@Test
+ @Config(minSdk = R)
+ public void getUserHandles() {
+ assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).size()).isEqualTo(1);
+ shadowOf(userManager).getUserHandles(/* excludeDying= */ true).get(0);
+ assertThat(UserHandle.myUserId()).isEqualTo(UserHandle.USER_SYSTEM);
+
+ UserHandle expectedUserHandle = shadowOf(userManager).addUser(10, "secondary_user", 0);
+ assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).size()).isEqualTo(2);
+ assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).get(1))
+ .isEqualTo(expectedUserHandle);
+ }
+
+ @Test
@Config(minSdk = N_MR1, maxSdk = Q)
public void isDemoUser() {
// All methods are based on the current user, so no need to pass a UserHandle.
@@ -565,6 +581,34 @@ public class ShadowUserManagerTest {
}
@Test
+ @Config(minSdk = Q)
+ public void removeSecondaryUser_noExistingUser_doesNotRemove() {
+ assertThat(shadowOf(userManager).removeUser(UserHandle.of(10))).isFalse();
+ assertThat(userManager.getUserCount()).isEqualTo(1);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void removeUserWhenPossible_twoUsersRemoveOne_hasOneUserLeft() {
+ shadowOf(userManager).addUser(10, "secondary_user", 0);
+ assertThat(
+ userManager.removeUserWhenPossible(
+ UserHandle.of(10), /* overrideDevicePolicy= */ false))
+ .isEqualTo(UserManager.REMOVE_RESULT_REMOVED);
+ assertThat(userManager.getUserCount()).isEqualTo(1);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void removeUserWhenPossible_nonExistingUser_fails() {
+ assertThat(
+ userManager.removeUserWhenPossible(
+ UserHandle.of(10), /* overrideDevicePolicy= */ false))
+ .isEqualTo(UserManager.REMOVE_RESULT_ERROR_UNKNOWN);
+ assertThat(userManager.getUserCount()).isEqualTo(1);
+ }
+
+ @Test
@Config(minSdk = JELLY_BEAN_MR1)
public void switchToSecondaryUser() {
shadowOf(userManager).addUser(10, "secondary_user", 0);
@@ -653,8 +697,8 @@ public class ShadowUserManagerTest {
@Config(minSdk = LOLLIPOP)
public void getProfiles_addedProfile_containsProfile() {
shadowOf(userManager).addUser(TEST_USER_HANDLE, "", 0);
- shadowOf(userManager).addProfile(
- TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS);
+ shadowOf(userManager)
+ .addProfile(TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS);
// getProfiles(userId) include user itself and asssociated profiles.
assertThat(userManager.getProfiles(TEST_USER_HANDLE).get(0).id).isEqualTo(TEST_USER_HANDLE);
@@ -850,7 +894,6 @@ public class ShadowUserManagerTest {
assertThat(UserManager.supportsMultipleUsers()).isTrue();
}
-
@Test
@Config(minSdk = Q)
public void getUserSwitchability_shouldReturnLastSetSwitchability() {
@@ -859,8 +902,7 @@ public class ShadowUserManagerTest {
.setUserSwitchability(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
assertThat(userManager.getUserSwitchability())
.isEqualTo(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
- shadowOf(userManager)
- .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
+ shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
assertThat(userManager.getUserSwitchability()).isEqualTo(UserManager.SWITCHABILITY_STATUS_OK);
}
@@ -880,8 +922,7 @@ public class ShadowUserManagerTest {
shadowOf(userManager)
.setUserSwitchability(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
assertThat(userManager.canSwitchUsers()).isFalse();
- shadowOf(userManager)
- .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
+ shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
assertThat(userManager.canSwitchUsers()).isTrue();
}
@@ -889,7 +930,7 @@ public class ShadowUserManagerTest {
@Config(minSdk = Q)
public void getUserName_shouldReturnSetUserName() {
shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
- shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags = */ 0);
+ shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags= */ 0);
shadowOf(userManager).switchUser(10);
assertThat(userManager.getUserName()).isEqualTo(PROFILE_USER_NAME);
}
@@ -900,7 +941,7 @@ public class ShadowUserManagerTest {
userManager.setUserIcon(TEST_USER_ICON);
assertThat(userManager.getUserIcon()).isEqualTo(TEST_USER_ICON);
- shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags = */ 0);
+ shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags= */ 0);
shadowOf(userManager).switchUser(10);
assertThat(userManager.getUserIcon()).isNull();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java
new file mode 100644
index 000000000..dbf7250ef
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java
@@ -0,0 +1,95 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Intent;
+import android.net.Ikev2VpnProfile;
+import android.net.VpnManager;
+import android.net.VpnProfileState;
+import android.os.Build.VERSION_CODES;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = VERSION_CODES.R)
+public class ShadowVpnManagerTest {
+ private VpnManager vpnManager;
+ private ShadowVpnManager shadowVpnManager;
+
+ @Before
+ public void setUp() throws Exception {
+ vpnManager = ApplicationProvider.getApplicationContext().getSystemService(VpnManager.class);
+ shadowVpnManager = shadowOf(vpnManager);
+ }
+
+ @Test
+ public void provisionVpnProfile() {
+ Intent intent = new Intent("foo");
+ shadowVpnManager.setProvisionVpnProfileResult(intent);
+
+ assertThat(
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity")
+ .setAuthPsk(new byte[0])
+ .build()))
+ .isSameInstanceAs(intent);
+
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+ VpnProfileState state = vpnManager.getProvisionedVpnProfileState();
+ assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_DISCONNECTED);
+ assertThat(state.getSessionId()).isNull();
+ }
+ }
+
+ @Test
+ public void deleteVpnProfile() {
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+ vpnManager.deleteProvisionedVpnProfile();
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void deleteVpnProfile_tiramisu() {
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+ assertThat(vpnManager.getProvisionedVpnProfileState()).isNotNull();
+
+ vpnManager.deleteProvisionedVpnProfile();
+ assertThat(vpnManager.getProvisionedVpnProfileState()).isNull();
+ }
+
+ @Test
+ public void startAndStopVpnProfile() {
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+ vpnManager.startProvisionedVpnProfile();
+ vpnManager.stopProvisionedVpnProfile();
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void startAndStopVpnProfile_tiramisu() {
+ vpnManager.provisionVpnProfile(
+ new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+ String sessionKey = vpnManager.startProvisionedVpnProfileSession();
+ VpnProfileState state = vpnManager.getProvisionedVpnProfileState();
+ assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_CONNECTED);
+ assertThat(state.getSessionId()).isEqualTo(sessionKey);
+ assertThat(state.isAlwaysOn()).isFalse();
+ assertThat(state.isLockdownEnabled()).isFalse();
+
+ vpnManager.stopProvisionedVpnProfile();
+ state = vpnManager.getProvisionedVpnProfileState();
+ assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_DISCONNECTED);
+ assertThat(state.getSessionId()).isNull();
+ assertThat(state.isAlwaysOn()).isFalse();
+ assertThat(state.isLockdownEnabled()).isFalse();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
index 7a1bee691..15dd9ec96 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
@@ -1,12 +1,17 @@
package org.robolectric.shadows;
+import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
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 androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -14,10 +19,12 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
+import android.app.Application;
import android.app.admin.DeviceAdminService;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.DhcpInfo;
import android.net.NetworkInfo;
@@ -27,13 +34,17 @@ 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.WifiManager.PnoScanResultsCallback;
+import android.net.wifi.WifiSsid;
import android.net.wifi.WifiUsabilityStatsEntry;
import android.os.Build;
import android.util.Pair;
-import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -47,9 +58,7 @@ public class ShadowWifiManagerTest {
@Before
public void setUp() throws Exception {
- wifiManager =
- (WifiManager)
- ApplicationProvider.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
}
@Test
@@ -494,8 +503,7 @@ public class ShadowWifiManagerTest {
// THEN
NetworkInfo networkInfo =
((ConnectivityManager)
- ApplicationProvider.getApplicationContext()
- .getSystemService(Context.CONNECTIVITY_SERVICE))
+ getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE))
.getActiveNetworkInfo();
assertThat(networkInfo.getType()).isEqualTo(ConnectivityManager.TYPE_WIFI);
assertThat(networkInfo.isConnected()).isTrue();
@@ -784,13 +792,305 @@ public class ShadowWifiManagerTest {
assertThat(shadowOf(wifiManager).getSoftApConfiguration().getSsid()).isEqualTo("foo");
}
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_nullCallback_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {3, 2, 5})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ /* callback= */ null));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_nullExecutor_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {3, 2, 5})),
+ /* frequencies= */ null,
+ /* executor= */ null,
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_nullSsidList_throwsIllegalStateException() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ /* ssids= */ null,
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_emptySsidList_throwsIllegalStateException() {
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ /* ssids= */ List.of(),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_moreThan2Ssids_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ List.of(
+ WifiSsid.fromBytes(new byte[] {1, 2, 3}),
+ WifiSsid.fromBytes(new byte[] {9, 8, 7, 6}),
+ WifiSsid.fromBytes(new byte[] {90, 81, 72, 63, 54})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_moreThan10Frequencies_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ wifiManager.setExternalPnoScanRequest(
+ List.of(
+ WifiSsid.fromBytes(new byte[] {1, 2, 3}),
+ WifiSsid.fromBytes(new byte[] {9, 8, 7, 6})),
+ new int[] {5160, 5180, 5200, 5220, 5240, 5260, 5280, 5300, 5320, 5340, 5360},
+ Executors.newSingleThreadExecutor(),
+ new TestPnoScanResultsCallback()));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_validRequest_successCallbackInvoked() throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ assertThat(callback.successfulRegistrations.take()).isNotNull();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void
+ setExternalPnoScanRequest_outstandingRequest_failureCallbackInvokedWithAlreadyRegisteredStatus()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {9, 2, 5})),
+ new int[] {5280},
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ assertThat(callback.failedRegistrations.take())
+ .isEqualTo(PnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setExternalPnoScanRequest_differentUid_failureCallbackInvokedWithBusyStatus()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ int firstAppUid = ShadowProcess.myUid();
+ int secondAppUid;
+ do {
+ secondAppUid = ShadowProcess.getRandomApplicationUid();
+ } while (firstAppUid == secondAppUid);
+ ShadowProcess.setUid(secondAppUid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+
+ assertThat(callback.failedRegistrations.take())
+ .isEqualTo(PnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void clearExternalPnoScanRequest_outstandingRequest_callbackInvokedWithUnregisteredStatus()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ Executors.newSingleThreadExecutor(),
+ callback);
+ wifiManager.clearExternalPnoScanRequest();
+
+ assertThat(callback.removedRegistrations.take())
+ .isEqualTo(PnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void clearExternalPnoScanRequest_wrongUid_callbackNotInvoked() throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+ /* frequencies= */ null,
+ executor,
+ callback);
+
+ int firstAppUid = ShadowProcess.myUid();
+ int secondAppUid;
+ do {
+ secondAppUid = ShadowProcess.getRandomApplicationUid();
+ } while (firstAppUid == secondAppUid);
+ ShadowProcess.setUid(secondAppUid);
+
+ wifiManager.clearExternalPnoScanRequest();
+
+ executor.shutdown();
+
+ assertThat(executor.awaitTermination(5, MINUTES)).isTrue();
+ assertThat(callback.removedRegistrations).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void networksFoundFromPnoScan_matchingSsid_availableCallbackInvoked() throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+ ScanResult scanResult = new ScanResult();
+ scanResult.setWifiSsid(wifiSsid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+ shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+ assertThat(callback.incomingScanResults.take()).containsExactly(scanResult);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void networksFoundFromPnoScan_matchingSsid_removedCallbackInvokedWithDeliveredStatus()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+ ScanResult scanResult = new ScanResult();
+ scanResult.setWifiSsid(wifiSsid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+ shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+ assertThat(callback.removedRegistrations.take())
+ .isEqualTo(PnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void networksFoundFromPnoScan_matchingSsid_scanResultsAvailableBroadcastSent() {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+ ScanResult scanResult = new ScanResult();
+ scanResult.setWifiSsid(wifiSsid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+ shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+ Intent expectedIntent = new Intent(SCAN_RESULTS_AVAILABLE_ACTION);
+ expectedIntent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
+ expectedIntent.setPackage(getApplicationContext().getPackageName());
+
+ assertThat(
+ shadowOf((Application) getApplicationContext()).getBroadcastIntents().stream()
+ .anyMatch(expectedIntent::filterEquals))
+ .isTrue();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void networksFoundFromPnoScan_noMatchingSsid_availableCallbackNotInvoked()
+ throws Exception {
+ TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+ WifiSsid otherWifiSsid = WifiSsid.fromBytes(new byte[] {9, 8, 7, 6});
+ ScanResult scanResult = new ScanResult();
+ scanResult.setWifiSsid(otherWifiSsid);
+
+ wifiManager.setExternalPnoScanRequest(
+ List.of(wifiSsid), /* frequencies= */ null, executor, callback);
+ shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+ executor.shutdown();
+
+ assertThat(executor.awaitTermination(5, MINUTES)).isTrue();
+ assertThat(callback.incomingScanResults).isEmpty();
+ }
+
+ private class TestPnoScanResultsCallback implements PnoScanResultsCallback {
+ LinkedBlockingQueue<List<ScanResult>> incomingScanResults = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue<Object> successfulRegistrations = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue<Integer> failedRegistrations = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue<Integer> removedRegistrations = new LinkedBlockingQueue<>();
+
+ @Override
+ public void onScanResultsAvailable(List<ScanResult> scanResults) {
+ incomingScanResults.add(scanResults);
+ }
+
+ @Override
+ public void onRegisterSuccess() {
+ successfulRegistrations.add(new Object());
+ }
+
+ @Override
+ public void onRegisterFailed(int reason) {
+ failedRegistrations.add(reason);
+ }
+
+ @Override
+ public void onRemoved(int reason) {
+ removedRegistrations.add(reason);
+ }
+ }
+
private void setDeviceOwner() {
shadowOf(
(DevicePolicyManager)
- ApplicationProvider.getApplicationContext()
- .getSystemService(Context.DEVICE_POLICY_SERVICE))
- .setDeviceOwner(
- new ComponentName(
- ApplicationProvider.getApplicationContext(), DeviceAdminService.class));
+ getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE))
+ .setDeviceOwner(new ComponentName(getApplicationContext(), DeviceAdminService.class));
}
}
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
index fac00226d..e1463a17b 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
@@ -10,6 +10,7 @@ import java.lang.invoke.MethodType;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.ListIterator;
+import java.util.Objects;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
@@ -212,23 +213,25 @@ public class ClassInstrumentor {
}
/**
- * Checks if the first instruction is a Jacoco load instructions. Robolectric is not capable at
- * the moment of re-instrumenting Jacoco-instrumented constructors.
+ * Checks if the first or second instruction is a Jacoco load instruction. Robolectric is not
+ * capable at the moment of re-instrumenting Jacoco-instrumented constructors, so these are
+ * currently skipped.
*
* @param ctor constructor method node
* @return whether or not the constructor can be instrumented
*/
private boolean isJacocoInstrumented(MethodNode ctor) {
AbstractInsnNode[] insns = ctor.instructions.toArray();
- if (insns.length > 0) {
- if (insns[0] instanceof LdcInsnNode
- && ((LdcInsnNode) insns[0]).cst instanceof ConstantDynamic) {
- ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) insns[0]).cst;
+ if (insns.length > 1) {
+ AbstractInsnNode node = insns[0];
+ if (node instanceof LabelNode) {
+ node = insns[1];
+ }
+ if ((node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof ConstantDynamic)) {
+ ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) node).cst;
return cst.getName().equals("$jacocoData");
- } else if (insns.length > 1
- && insns[0] instanceof LabelNode
- && insns[1] instanceof MethodInsnNode) {
- return "$jacocoInit".equals(((MethodInsnNode) insns[1]).name);
+ } else if (node instanceof MethodInsnNode) {
+ return Objects.equals(((MethodInsnNode) node).name, "$jacocoInit");
}
}
return false;
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..73c36a542
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
@@ -0,0 +1,117 @@
+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) {
+ 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*/),
+ 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/CellInfoLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
new file mode 100644
index 000000000..412d8c72f
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
@@ -0,0 +1,139 @@
+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() {
+ if (cellIdentity == null) {
+ cellIdentity = CellIdentityLteBuilder.getDefaultInstance();
+ }
+ if (cellSignalStrength == null) {
+ cellSignalStrength = CellSignalStrengthLteBuilder.getDefaultInstance();
+ }
+ int apiLevel = RuntimeEnvironment.getApiLevel();
+ 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/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/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/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/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..00bb9558c 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,7 +95,18 @@ 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) {
@@ -141,24 +152,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 +238,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 +250,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 +291,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..f7f5f5b8c 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;
@@ -88,20 +87,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/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/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/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..416033b9b 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
@@ -210,8 +214,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() {
@@ -340,6 +364,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 +403,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..fb5c1c295 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
@@ -405,6 +405,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/ShadowTelephonyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
index 0864354d9..0ab373054 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -63,9 +63,11 @@ 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 +143,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;
@@ -263,6 +266,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 +1225,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();
}
/**
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/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/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);
+ }
+ }
+ }
}
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java
new file mode 100644
index 000000000..d69c39145
--- /dev/null
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java
@@ -0,0 +1,11 @@
+package org.robolectric.util.reflector;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Indicates that the annotated method is a constructor. */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Constructor {}
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
index 2873e2864..12f855f2b 100644
--- a/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
@@ -38,6 +38,8 @@ public class Reflector {
private static final boolean DEBUG = false;
private static final AtomicInteger COUNTER = new AtomicInteger();
private static final Map<Class<?>, Constructor<?>> cache = new ConcurrentHashMap<>();
+ private static final Map<Class<?>, Object> staticReflectorCache = new ConcurrentHashMap<>();
+
/**
* Returns an object which provides accessors for invoking otherwise inaccessible static methods
* and fields.
@@ -56,6 +58,10 @@ public class Reflector {
* @param target the target object
*/
public static <T> T reflector(Class<T> iClass, Object target) {
+ if (target == null && staticReflectorCache.containsKey(iClass)) {
+ return (T) staticReflectorCache.get(iClass);
+ }
+
Class<?> targetClass = determineTargetClass(iClass);
Constructor<? extends T> ctor = (Constructor<? extends T>) cache.get(iClass);
@@ -68,11 +74,15 @@ public class Reflector {
() -> Reflector.<T>createReflectorClass(iClass, targetClass));
ctor = reflectorClass.getConstructor(targetClass);
ctor.setAccessible(true);
+ cache.put(iClass, ctor);
}
- cache.put(iClass, ctor);
+ T instance = ctor.newInstance(target);
+ if (target == null) {
+ staticReflectorCache.put(iClass, instance);
+ }
+ return instance;
- return ctor.newInstance(target);
} catch (NoSuchMethodException
| InstantiationException
| IllegalAccessException
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
index d3e366855..ea9b45c6d 100644
--- a/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
@@ -15,6 +15,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
+import javax.annotation.Nullable;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
@@ -29,6 +30,8 @@ class ReflectorClassWriter extends ClassWriter {
private static final Type CLASS_TYPE = Type.getType(Class.class);
private static final Type FIELD_TYPE = Type.getType(Field.class);
private static final Type METHOD_TYPE = Type.getType(Method.class);
+ private static final Type CONSTRUCTOR_TYPE = Type.getType(java.lang.reflect.Constructor.class);
+
private static final Type STRING_TYPE = Type.getType(String.class);
private static final Type STRINGBUILDER_TYPE = Type.getType(StringBuilder.class);
@@ -45,6 +48,8 @@ class ReflectorClassWriter extends ClassWriter {
findMethod(Class.class, "getDeclaredField", new Class<?>[] {String.class});
private static final org.objectweb.asm.commons.Method CLASS$GET_DECLARED_METHOD =
findMethod(Class.class, "getDeclaredMethod", new Class<?>[] {String.class, Class[].class});
+ private static final org.objectweb.asm.commons.Method CLASS$GET_DECLARED_CONSTRUCTOR =
+ findMethod(Class.class, "getDeclaredConstructor", new Class<?>[] {Class[].class});
private static final org.objectweb.asm.commons.Method ACCESSIBLE_OBJECT$SET_ACCESSIBLE =
findMethod(AccessibleObject.class, "setAccessible", new Class<?>[] {boolean.class});
private static final org.objectweb.asm.commons.Method FIELD$GET =
@@ -53,6 +58,9 @@ class ReflectorClassWriter extends ClassWriter {
findMethod(Field.class, "set", new Class<?>[] {Object.class, Object.class});
private static final org.objectweb.asm.commons.Method METHOD$INVOKE =
findMethod(Method.class, "invoke", new Class<?>[] {Object.class, Object[].class});
+ private static final org.objectweb.asm.commons.Method CONSTRUCTOR$NEWINSTANCE =
+ findMethod(
+ java.lang.reflect.Constructor.class, "newInstance", new Class<?>[] {Object[].class});
private static final org.objectweb.asm.commons.Method THROWABLE$GET_CAUSE =
findMethod(Throwable.class, "getCause", new Class<?>[] {});
private static final org.objectweb.asm.commons.Method OBJECT_INIT =
@@ -118,8 +126,11 @@ class ReflectorClassWriter extends ClassWriter {
if (method.isDefault()) continue;
Accessor accessor = method.getAnnotation(Accessor.class);
+ Constructor constructor = method.getAnnotation(Constructor.class);
if (accessor != null) {
new AccessorMethodWriter(method, accessor).write();
+ } else if (constructor != null) {
+ new ConstructorMethodWriter(method).write();
} else {
new ReflectorMethodWriter(method).write();
}
@@ -251,6 +262,135 @@ class ReflectorClassWriter extends ClassWriter {
}
}
+ private class ConstructorMethodWriter extends BaseAdapter {
+
+ private final String constructorRefName;
+ private final Type[] targetParamTypes;
+
+ private ConstructorMethodWriter(Method method) {
+ super(method);
+ int myMethodNumber = nextMethodNumber++;
+ this.constructorRefName = "constructor" + myMethodNumber;
+ this.targetParamTypes = resolveParamTypes(iMethod);
+ }
+
+ void write() {
+ // write field to hold method reference...
+ visitField(
+ ACC_PRIVATE | ACC_STATIC,
+ constructorRefName,
+ CONSTRUCTOR_TYPE.getDescriptor(),
+ null,
+ null);
+
+ visitCode();
+
+ // pseudocode:
+ // try {
+ // return constructorN.newInstance(*args);
+ // } catch (InvocationTargetException e) {
+ // throw e.getCause();
+ // } catch (ReflectiveOperationException e) {
+ // throw new AssertionError("Error invoking reflector method in ClassLoader " +
+ // Instrumentation.class.getClassLoader(), e);
+ // }
+ Label tryStart = new Label();
+ Label tryEnd = new Label();
+ Label handleInvocationTargetException = new Label();
+ visitTryCatchBlock(
+ tryStart,
+ tryEnd,
+ handleInvocationTargetException,
+ INVOCATION_TARGET_EXCEPTION_TYPE.getInternalName());
+ Label handleReflectiveOperationException = new Label();
+ visitTryCatchBlock(
+ tryStart,
+ tryEnd,
+ handleReflectiveOperationException,
+ REFLECTIVE_OPERATION_EXCEPTION_TYPE.getInternalName());
+
+ mark(tryStart);
+ loadOriginalConstructorRef();
+ loadArgArray();
+ invokeVirtual(CONSTRUCTOR_TYPE, CONSTRUCTOR$NEWINSTANCE);
+ mark(tryEnd);
+
+ castForReturn(iMethod.getReturnType());
+ returnValue();
+
+ mark(handleInvocationTargetException);
+
+ int exceptionLocalVar = newLocal(THROWABLE_TYPE);
+ storeLocal(exceptionLocalVar);
+ loadLocal(exceptionLocalVar);
+ invokeVirtual(THROWABLE_TYPE, THROWABLE$GET_CAUSE);
+ throwException();
+ mark(handleReflectiveOperationException);
+ exceptionLocalVar = newLocal(REFLECTIVE_OPERATION_EXCEPTION_TYPE);
+ storeLocal(exceptionLocalVar);
+ newInstance(STRINGBUILDER_TYPE);
+ dup();
+ invokeConstructor(STRINGBUILDER_TYPE, OBJECT_INIT);
+ push("Error invoking reflector method in ClassLoader ");
+ invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$APPEND);
+ push(targetType);
+ invokeVirtual(CLASS_TYPE, CLASS$GET_CLASS_LOADER);
+ invokeStatic(STRING_TYPE, STRING$VALUE_OF);
+ invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$APPEND);
+ invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$TO_STRING);
+ int messageLocalVar = newLocal(STRING_TYPE);
+ storeLocal(messageLocalVar);
+ newInstance(ASSERTION_ERROR_TYPE);
+ dup();
+ loadLocal(messageLocalVar);
+ loadLocal(exceptionLocalVar);
+ invokeConstructor(ASSERTION_ERROR_TYPE, ASSERTION_ERROR_INIT);
+ throwException();
+
+ endMethod();
+ }
+
+ private void loadOriginalConstructorRef() {
+ // pseudocode:
+ // if (constructorN == null) {
+ // constructorN = targetClass.getDeclaredConstructor(paramTypes);
+ // constructorN.setAccessible(true);
+ // }
+ // -> constructor reference on stack
+ getStatic(reflectorType, constructorRefName, CONSTRUCTOR_TYPE);
+ dup();
+ Label haveConstructorRef = newLabel();
+ ifNonNull(haveConstructorRef);
+ pop();
+
+ // pseudocode:
+ // targetClass.getDeclaredConstructor(paramTypes);
+ push(targetType);
+ Type[] paramTypes = targetParamTypes;
+ push(paramTypes.length);
+ newArray(CLASS_TYPE);
+ for (int i = 0; i < paramTypes.length; i++) {
+ dup();
+ push(i);
+ push(paramTypes[i]);
+ arrayStore(CLASS_TYPE);
+ }
+ invokeVirtual(CLASS_TYPE, CLASS$GET_DECLARED_CONSTRUCTOR);
+
+ // pseudocode:
+ // <constructor>.setAccessible(true);
+ dup();
+ push(true);
+ invokeVirtual(CONSTRUCTOR_TYPE, ACCESSIBLE_OBJECT$SET_ACCESSIBLE);
+
+ // pseudocode:
+ // constructorN = constructor;
+ dup();
+ putStatic(reflectorType, constructorRefName, CONSTRUCTOR_TYPE);
+ mark(haveConstructorRef);
+ }
+ }
+
private class ReflectorMethodWriter extends BaseAdapter {
private final String methodRefName;
@@ -375,35 +515,6 @@ class ReflectorClassWriter extends ClassWriter {
putStatic(reflectorType, methodRefName, METHOD_TYPE);
mark(haveMethodRef);
}
-
- private Type[] resolveParamTypes(Method iMethod) {
- Class<?>[] iParamTypes = iMethod.getParameterTypes();
- Annotation[][] paramAnnotations = iMethod.getParameterAnnotations();
-
- Type[] targetParamTypes = new Type[iParamTypes.length];
- for (int i = 0; i < iParamTypes.length; i++) {
- Class<?> paramType = findWithType(paramAnnotations[i]);
- if (paramType == null) {
- paramType = iParamTypes[i];
- }
- targetParamTypes[i] = Type.getType(paramType);
- }
- return targetParamTypes;
- }
-
- private Class<?> findWithType(Annotation[] paramAnnotation) {
- for (Annotation annotation : paramAnnotation) {
- if (annotation instanceof WithType) {
- String withTypeName = ((WithType) annotation).value();
- try {
- return Class.forName(withTypeName, true, iClass.getClassLoader());
- } catch (ClassNotFoundException e1) {
- // it's okay, ignore
- }
- }
- }
- return null;
- }
}
private static String[] getInternalNames(final Class<?>[] types) {
@@ -494,5 +605,35 @@ class ReflectorClassWriter extends ClassWriter {
void loadNull() {
visitInsn(Opcodes.ACONST_NULL);
}
+
+ protected Type[] resolveParamTypes(Method iMethod) {
+ Class<?>[] iParamTypes = iMethod.getParameterTypes();
+ Annotation[][] paramAnnotations = iMethod.getParameterAnnotations();
+
+ Type[] targetParamTypes = new Type[iParamTypes.length];
+ for (int i = 0; i < iParamTypes.length; i++) {
+ Class<?> paramType = findWithType(paramAnnotations[i]);
+ if (paramType == null) {
+ paramType = iParamTypes[i];
+ }
+ targetParamTypes[i] = Type.getType(paramType);
+ }
+ return targetParamTypes;
+ }
+
+ @Nullable
+ private Class<?> findWithType(Annotation[] paramAnnotation) {
+ for (Annotation annotation : paramAnnotation) {
+ if (annotation instanceof WithType) {
+ String withTypeName = ((WithType) annotation).value();
+ try {
+ return Class.forName(withTypeName, true, iClass.getClassLoader());
+ } catch (ClassNotFoundException e1) {
+ // it's okay, ignore
+ }
+ }
+ }
+ return null;
+ }
}
}
diff --git a/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java b/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
index 8baf3d63e..74dc88487 100644
--- a/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
+++ b/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
@@ -133,6 +133,25 @@ public class ReflectorTest {
time("saved accessor", 10_000_000, () -> fieldBySavedReflector(accessor));
}
+ @Ignore
+ @Test
+ public void constructorPerf() {
+ SomeClass i = new SomeClass("c");
+
+ System.out.println("reflection = " + Collections.singletonList(methodByReflectionHelpers(i)));
+ System.out.println("accessor = " + Collections.singletonList(methodByReflector(i)));
+
+ _SomeClass_ accessor = reflector(_SomeClass_.class, i);
+
+ time("ReflectionHelpers", 10_000_000, this::constructorByReflectionHelpers);
+ time("accessor", 10_000_000, () -> constructorByReflector());
+ time("saved accessor", 10_000_000, () -> constructorBySavedReflector(accessor));
+
+ time("ReflectionHelpers", 10_000_000, () -> constructorByReflectionHelpers());
+ time("accessor", 10_000_000, () -> constructorByReflector());
+ time("saved accessor", 10_000_000, () -> constructorBySavedReflector(accessor));
+ }
+
@Test
public void nonExistentMethod_throwsAssertionError() {
SomeClass i = new SomeClass("c");
@@ -143,6 +162,11 @@ public class ReflectorTest {
assertThat(ex).hasCauseThat().isInstanceOf(NoSuchMethodException.class);
}
+ @Test
+ public void reflector_constructor() {
+ assertThat(staticReflector.newSomeClass("sdfsdf")).isNotNull();
+ }
+
//////////////////////
/** Accessor interface for {@link SomeClass}'s internals. */
@@ -170,6 +194,9 @@ public class ReflectorTest {
@Accessor("mD")
int getD();
+ @Constructor
+ SomeClass newSomeClass(String c);
+
String someMethod(String a, String b);
String nonExistentMethod(String a, String b, String c);
@@ -251,6 +278,20 @@ public class ReflectorTest {
return reflector.someMethod("a", "b");
}
+ private SomeClass constructorByReflectionHelpers() {
+ return ReflectionHelpers.callConstructor(
+ SomeClass.class, ClassParameter.from(String.class, "a"));
+ }
+
+ private SomeClass constructorByReflector() {
+ _SomeClass_ accessor = reflector(_SomeClass_.class);
+ return accessor.newSomeClass("a");
+ }
+
+ private SomeClass constructorBySavedReflector(_SomeClass_ reflector) {
+ return reflector.newSomeClass("a");
+ }
+
private String fieldByReflectionHelpers(SomeClass o) {
ReflectionHelpers.setField(o, "c", "abc");
return ReflectionHelpers.getField(o, "c");