aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-05-09 01:25:21 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2024-05-09 01:25:21 +0000
commit6ac624955b748f8a3a3b5595f1d6ffbc85a170eb (patch)
tree9b3ab586b33552f7346a35e83656da0023d3501c
parentf609eaec10ee5793f6e046915da5042ad7ee9e5e (diff)
parent746f685a324dade8ef79b673aa9379de9f5c5c3c (diff)
downloadrobolectric-sdk-release.tar.gz
Merge "Snap for 11819063 from be17efc0a8d51d690f8290bc32049b4eb8b4fc18 to sdk-release" into sdk-releasesdk-release
-rw-r--r--gradle/libs.versions.toml2
-rw-r--r--integration_tests/kotlin/build.gradle1
-rw-r--r--integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt79
-rw-r--r--integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt83
-rw-r--r--resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java5
-rw-r--r--resources/src/main/java/org/robolectric/res/android/DataType.java4
-rw-r--r--resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java14
-rw-r--r--resources/src/test/java/org/robolectric/res/android/DataTypeTest.java23
-rw-r--r--resources/src/test/java/org/robolectric/res/android/DynamicRefTableTest.java23
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowCaptioningManagerTest.java155
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java14
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java12
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java17
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/HardwareRenderingScreenshot.java14
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothLeAdvertiser.java52
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCaptioningManager.java80
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInCallService.java4
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java306
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java19
20 files changed, 696 insertions, 217 deletions
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 52a90b6aa..ca186969d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-robolectric-nativeruntime-dist-compat = "1.0.9"
+robolectric-nativeruntime-dist-compat = "1.0.10"
# https://developer.android.com/studio/releases
android-gradle = "8.3.1"
diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle
index c95d64f3f..37686d981 100644
--- a/integration_tests/kotlin/build.gradle
+++ b/integration_tests/kotlin/build.gradle
@@ -18,6 +18,7 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
testImplementation libs.kotlin.stdlib
+ testImplementation libs.kotlinx.coroutines.android
testImplementation libs.junit4
testImplementation libs.truth
testImplementation "androidx.test:core:$axtCoreVersion@aar"
diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt
new file mode 100644
index 000000000..2094dd13b
--- /dev/null
+++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisioner.kt
@@ -0,0 +1,79 @@
+package org.robolectric.integrationtests.kotlin.flow
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCallback
+import android.bluetooth.BluetoothGattService
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothProfile
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanResult
+import android.content.Context
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flow
+
+/** A class that invokes Android Bluetooth LE APIs. */
+class BluetoothProvisioner(applicationContext: Context) {
+
+ val context: Context
+
+ init {
+ context = applicationContext
+ }
+
+ fun startScan(): Flow<BluetoothDevice> = callbackFlow {
+ val scanCallback =
+ object : ScanCallback() {
+ override fun onScanResult(callbackType: Int, result: ScanResult?) {
+ if (result?.device != null) {
+ val unused = trySend(result.device)
+ }
+ }
+
+ override fun onScanFailed(errorCode: Int) {
+ cancel("BLE Scan Failed", null)
+ }
+ }
+ val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ val scanner = bluetoothManager.adapter.bluetoothLeScanner
+ scanner.startScan(scanCallback)
+ awaitClose { scanner.stopScan(scanCallback) }
+ }
+
+ fun connectToDevice(device: BluetoothDevice): Flow<BluetoothGatt> = callbackFlow {
+ val gattCallback =
+ object : BluetoothGattCallback() {
+ override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ val unused = gatt!!.discoverServices()
+ } else {
+ cancel("Connect Failed", null)
+ }
+ }
+
+ override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ val unused = trySend(gatt!!)
+ } else {
+ cancel("Service discovery failed", null)
+ }
+ }
+ }
+
+ device.connectGatt(context, true, gattCallback)
+ awaitClose {}
+ }
+
+ fun scanAndConnect() =
+ flow<BluetoothGattService> {
+ val device = startScan().firstOrNull()
+ if (device != null) {
+ val gatt = connectToDevice(device).firstOrNull()
+ emit(gatt!!.services[0])
+ }
+ }
+}
diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt
new file mode 100644
index 000000000..181b6a8b9
--- /dev/null
+++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/flow/BluetoothProvisionerTest.kt
@@ -0,0 +1,83 @@
+package org.robolectric.integrationtests.kotlin.flow
+
+import android.bluetooth.BluetoothGattService
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.ScanResult
+import android.content.Context
+import android.os.Build.VERSION_CODES.S
+import com.google.common.truth.Truth.assertThat
+import java.util.UUID
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.android.util.concurrent.PausedExecutorService
+import org.robolectric.annotation.Config
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowBluetoothDevice
+import org.robolectric.shadows.ShadowBluetoothGatt
+import org.robolectric.shadows.ShadowBluetoothLeScanner
+
+/**
+ * A test that uses a custom executor-backed coroutine dispatcher to control the execution of
+ * coroutines.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [S])
+class BluetoothProvisionerTest {
+
+ val BLUETOOTH_MAC = "00:11:22:33:AA:BB"
+
+ val context = RuntimeEnvironment.getApplication()
+
+ val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+
+ fun newScanResult(): ScanResult {
+ val bluetoothDevice = bluetoothManager.adapter.getRemoteDevice(BLUETOOTH_MAC)
+ return ScanResult(bluetoothDevice, null, 0, 0)
+ }
+
+ @Test
+ fun testBluetoothProvisioner() {
+ val executor = PausedExecutorService()
+ val dispatcher = executor.asCoroutineDispatcher()
+ val scope = CoroutineScope(dispatcher)
+
+ scope.launch {
+ val gattService =
+ BluetoothProvisioner(RuntimeEnvironment.getApplication()).scanAndConnect().firstOrNull()
+ assertThat(gattService).isNotNull()
+ }
+
+ executor.runAll()
+
+ val scanner = bluetoothManager.adapter.bluetoothLeScanner
+ val shadowScanner = Shadow.extract<ShadowBluetoothLeScanner>(scanner)
+
+ val scanResult = newScanResult()
+ val bluetoothDevice = scanResult.device
+ shadowScanner.scanCallbacks.first().onScanResult(0, newScanResult())
+
+ executor.runAll()
+
+ val shadowDevice = Shadow.extract<ShadowBluetoothDevice>(bluetoothDevice)
+
+ val gatt = shadowDevice.bluetoothGatts.first()
+ val shadowGatt = Shadow.extract<ShadowBluetoothGatt>(gatt)
+
+ val service =
+ BluetoothGattService(
+ UUID.fromString("00000000-0000-0000-0000-0000000000A1"),
+ BluetoothGattService.SERVICE_TYPE_PRIMARY,
+ )
+
+ shadowGatt.addDiscoverableService(service)
+ shadowGatt.notifyConnection(BLUETOOTH_MAC)
+
+ executor.runAll()
+ }
+}
diff --git a/resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java b/resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java
index 803f82363..92280825e 100644
--- a/resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java
+++ b/resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java
@@ -885,7 +885,10 @@ public class CppAssetManager2 {
out_value.set(device_value.copy());
// Convert the package ID to the runtime assigned package ID.
- entry.get().dynamic_ref_table.lookupResourceValue(out_value);
+ int err = entry.get().dynamic_ref_table.lookupResourceValue(out_value);
+ if (err != NO_ERROR) {
+ return K_INVALID_COOKIE;
+ }
out_selected_config.set(new ResTable_config(entry.get().config));
out_flags.set(entry.get().type_flags);
diff --git a/resources/src/main/java/org/robolectric/res/android/DataType.java b/resources/src/main/java/org/robolectric/res/android/DataType.java
index 30938926a..b238544b6 100644
--- a/resources/src/main/java/org/robolectric/res/android/DataType.java
+++ b/resources/src/main/java/org/robolectric/res/android/DataType.java
@@ -70,6 +70,8 @@ public enum DataType {
}
public static DataType fromCode(byte code) {
- return Preconditions.checkNotNull(FROM_BYTE.get(code), "Unknown resource type: %s", code);
+ DataType type = FROM_BYTE.get(code);
+ Preconditions.checkArgument(type != null, "Unknown resource type: %s", code);
+ return type;
}
}
diff --git a/resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java b/resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java
index 0604116be..537a9f2f2 100644
--- a/resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java
+++ b/resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java
@@ -1,7 +1,9 @@
package org.robolectric.res.android;
-// transliterated from https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/include/androidfw/ResourceTypes.h
+// transliterated from
+// https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/include/androidfw/ResourceTypes.h
+import static org.robolectric.res.android.Errors.BAD_TYPE;
import static org.robolectric.res.android.Errors.NO_ERROR;
import static org.robolectric.res.android.Errors.UNKNOWN_ERROR;
import static org.robolectric.res.android.ResTable.APP_PACKAGE_ID;
@@ -145,7 +147,15 @@ public class DynamicRefTable
int lookupResourceValue(Ref<Res_value> value) {
byte resolvedType = DataType.REFERENCE.code();
Res_value inValue = value.get();
- switch (DataType.fromCode(inValue.dataType)) {
+
+ DataType dataType;
+ try {
+ dataType = DataType.fromCode(inValue.dataType);
+ } catch (IllegalArgumentException e) {
+ return BAD_TYPE;
+ }
+
+ switch (dataType) {
case ATTRIBUTE:
resolvedType = DataType.ATTRIBUTE.code();
// fallthrough
diff --git a/resources/src/test/java/org/robolectric/res/android/DataTypeTest.java b/resources/src/test/java/org/robolectric/res/android/DataTypeTest.java
new file mode 100644
index 000000000..3dea9d35e
--- /dev/null
+++ b/resources/src/test/java/org/robolectric/res/android/DataTypeTest.java
@@ -0,0 +1,23 @@
+package org.robolectric.res.android;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class DataTypeTest {
+
+ @Test
+ public void fromCode_shouldThrowExceptionForInvalidCode() {
+ assertThrows(IllegalArgumentException.class, () -> DataType.fromCode(99));
+ }
+
+ @Test
+ public void fromCode_shouldReturnCorrectDataTypeForValidCode() {
+ assertThat(DataType.fromCode(0)).isEqualTo(DataType.NULL);
+ assertThat(DataType.fromCode(3)).isEqualTo(DataType.STRING);
+ }
+}
diff --git a/resources/src/test/java/org/robolectric/res/android/DynamicRefTableTest.java b/resources/src/test/java/org/robolectric/res/android/DynamicRefTableTest.java
new file mode 100644
index 000000000..f03578626
--- /dev/null
+++ b/resources/src/test/java/org/robolectric/res/android/DynamicRefTableTest.java
@@ -0,0 +1,23 @@
+package org.robolectric.res.android;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.res.android.Errors.BAD_TYPE;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.robolectric.res.android.ResourceTypes.Res_value;
+
+@RunWith(JUnit4.class)
+public final class DynamicRefTableTest {
+
+ private static final Ref<Res_value> RES_VALUE_OF_BAD_TYPE =
+ new Ref<>(new Res_value(/* dataType= */ (byte) 99, /* data= */ 0));
+
+ @Test
+ public void lookupResourceValue_returnsBadTypeIfTypeOutOfEnumRange() {
+ DynamicRefTable pseudoRefTable =
+ new DynamicRefTable(/* packageId= */ (byte) 0, /* appAsLib= */ true);
+ assertThat(pseudoRefTable.lookupResourceValue(RES_VALUE_OF_BAD_TYPE)).isEqualTo(BAD_TYPE);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowCaptioningManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowCaptioningManagerTest.java
index 470eeff75..bb9e408fd 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowCaptioningManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowCaptioningManagerTest.java
@@ -1,31 +1,81 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
+import android.os.Looper;
+import android.provider.Settings;
+import android.provider.Settings.Secure;
import android.view.accessibility.CaptioningManager;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
+import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.Locale;
+import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
/** Tests for the ShadowCaptioningManager. */
@RunWith(AndroidJUnit4.class)
-@Config(minSdk = 19)
+@Config(minSdk = KITKAT)
public final class ShadowCaptioningManagerTest {
- @Mock private CaptioningChangeListener captioningChangeListener;
+ private TestCaptioningChangeListener captioningChangeListener =
+ new TestCaptioningChangeListener();
+
+ private static final int ENABLED = 1;
+ private static final int DISABLED = 0;
private CaptioningManager captioningManager;
+ private Context context;
+
+ public class TestCaptioningChangeListener extends CaptioningChangeListener {
+ public boolean isEnabled = false;
+ @Nullable public CaptionStyle captionStyle = null;
+ @Nullable public Locale locale = null;
+ public float fontScale = 1.0f;
+ public boolean systemAudioCaptioningEnabled = false;
+ public boolean systemAudioCaptioningUiEnabled = false;
+
+ @Override
+ public void onEnabledChanged(boolean enabled) {
+ isEnabled = enabled;
+ }
+
+ @Override
+ public void onUserStyleChanged(@NonNull CaptionStyle userStyle) {
+ captionStyle = userStyle;
+ }
+
+ @Override
+ public void onLocaleChanged(@Nullable Locale locale) {
+ this.locale = locale;
+ }
+
+ @Override
+ public void onFontScaleChanged(float fontScale) {
+ this.fontScale = fontScale;
+ }
+
+ @Override
+ public void onSystemAudioCaptioningChanged(boolean enabled) {
+ this.systemAudioCaptioningEnabled = enabled;
+ }
+
+ @Override
+ public void onSystemAudioCaptioningUiChanged(boolean enabled) {
+ this.systemAudioCaptioningUiEnabled = enabled;
+ }
+ }
@Before
public void setUp() {
@@ -34,87 +84,106 @@ public final class ShadowCaptioningManagerTest {
(CaptioningManager)
ApplicationProvider.getApplicationContext()
.getSystemService(Context.CAPTIONING_SERVICE);
+ context = RuntimeEnvironment.getApplication();
}
@Test
public void setEnabled_true() {
- assertThat(captioningManager.isEnabled()).isFalse();
-
- shadowOf(captioningManager).setEnabled(true);
+ Settings.Secure.putInt(
+ context.getContentResolver(), Secure.ACCESSIBILITY_CAPTIONING_ENABLED, ENABLED);
assertThat(captioningManager.isEnabled()).isTrue();
}
@Test
public void setEnabled_false() {
- shadowOf(captioningManager).setEnabled(false);
+ Settings.Secure.putInt(
+ context.getContentResolver(), Secure.ACCESSIBILITY_CAPTIONING_ENABLED, DISABLED);
assertThat(captioningManager.isEnabled()).isFalse();
}
@Test
- public void setFontScale_changesValueOfGetFontScale() {
- float fontScale = 1.5f;
- shadowOf(captioningManager).setFontScale(fontScale);
+ public void setEnabled_callsCallback() {
+ captioningManager.addCaptioningChangeListener(captioningChangeListener);
+ Settings.Secure.putInt(
+ context.getContentResolver(), Secure.ACCESSIBILITY_CAPTIONING_ENABLED, ENABLED);
- assertThat(captioningManager.getFontScale()).isWithin(0.001f).of(fontScale);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(captioningChangeListener.isEnabled).isTrue();
}
@Test
- public void setFontScale_notifiesObservers() {
- float fontScale = 1.5f;
- captioningManager.addCaptioningChangeListener(captioningChangeListener);
-
- shadowOf(captioningManager).setFontScale(fontScale);
+ public void setFontScale_updatesValue() {
+ Settings.Secure.putFloat(
+ context.getContentResolver(), Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 2.0f);
- verify(captioningChangeListener).onFontScaleChanged(fontScale);
+ assertThat(captioningManager.getFontScale()).isEqualTo(2.0f);
}
@Test
- public void addCaptioningChangeListener_doesNotRegisterSameListenerTwice() {
- float fontScale = 1.5f;
+ public void setFontScale_callsCallback() {
captioningManager.addCaptioningChangeListener(captioningChangeListener);
+ Settings.Secure.putFloat(
+ context.getContentResolver(), Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE, 3.0f);
- captioningManager.addCaptioningChangeListener(captioningChangeListener);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(captioningChangeListener.fontScale).isEqualTo(3.0f);
+ }
- shadowOf(captioningManager).setFontScale(fontScale);
- verify(captioningChangeListener).onFontScaleChanged(fontScale);
+ @Test
+ public void setLocale_updatesValue() {
+ Settings.Secure.putString(
+ context.getContentResolver(),
+ Secure.ACCESSIBILITY_CAPTIONING_LOCALE,
+ Locale.JAPANESE.toLanguageTag());
+
+ assertThat(captioningManager.getLocale()).isEqualTo(Locale.JAPANESE);
}
@Test
- public void removeCaptioningChangeListener_unregistersFontScaleListener() {
+ public void setLocale_callsCallback() {
captioningManager.addCaptioningChangeListener(captioningChangeListener);
+ Settings.Secure.putString(
+ context.getContentResolver(),
+ Secure.ACCESSIBILITY_CAPTIONING_LOCALE,
+ Locale.FRENCH.toLanguageTag());
- captioningManager.removeCaptioningChangeListener(captioningChangeListener);
-
- shadowOf(captioningManager).setFontScale(1.5f);
- verifyNoMoreInteractions(captioningChangeListener);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(captioningChangeListener.locale).isEqualTo(Locale.FRENCH);
}
@Test
- public void setLocale_nonNull() {
- Locale locale = Locale.US;
- assertThat(captioningManager.getLocale()).isNull();
-
- shadowOf(captioningManager).setLocale(locale);
+ @Config(minSdk = TIRAMISU)
+ public void setSystemAudioCaptioningEnabled_updatesValue() {
+ captioningManager.setSystemAudioCaptioningEnabled(true);
- assertThat(captioningManager.getLocale()).isEqualTo(locale);
+ assertThat(captioningManager.isSystemAudioCaptioningEnabled()).isEqualTo(true);
}
@Test
- public void setLocale_null() {
- shadowOf(captioningManager).setLocale(null);
+ @Config(minSdk = TIRAMISU)
+ public void setSystemAudioCaptioningEnabled_callsCallback() {
+ captioningManager.setSystemAudioCaptioningEnabled(false);
- assertThat(captioningManager.getLocale()).isNull();
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(captioningChangeListener.systemAudioCaptioningEnabled).isEqualTo(false);
}
@Test
- public void setLocale_notifiesObservers() {
- Locale locale = Locale.US;
- captioningManager.addCaptioningChangeListener(captioningChangeListener);
+ @Config(minSdk = TIRAMISU)
+ public void setSystemAudioCaptioningUiEnabled_updatesValue() {
+ captioningManager.setSystemAudioCaptioningUiEnabled(true);
+
+ assertThat(captioningManager.isSystemAudioCaptioningUiEnabled()).isEqualTo(true);
+ }
- shadowOf(captioningManager).setLocale(locale);
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void setSystemAudioCaptioningUiEnabled_callsCallback() {
+ captioningManager.setSystemAudioCaptioningUiEnabled(false);
- verify(captioningChangeListener).onLocaleChanged(locale);
+ shadowOf(Looper.getMainLooper()).idle();
+ assertThat(captioningChangeListener.systemAudioCaptioningUiEnabled).isEqualTo(false);
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java
index 4da4963cd..459b75b65 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowEGL14Test.java
@@ -27,8 +27,10 @@ public final class ShadowEGL14Test {
}
@Test
- public void eglChooseConfig() {
+ public void eglChooseConfig_retrieveConfigs() {
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ // When the config array is not null, numConfig is filled with the number of configs returned
+ // (up to the size of the config array).
EGLConfig[] configs = new EGLConfig[1];
int[] numConfig = new int[1];
assertThat(EGL14.eglChooseConfig(display, new int[0], 0, configs, 0, 1, numConfig, 0)).isTrue();
@@ -37,6 +39,16 @@ public final class ShadowEGL14Test {
}
@Test
+ public void eglChooseConfig_countMatching() {
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ // When the config array is null, numConfig is filled with the total number of configs matched.
+ EGLConfig[] configs = null;
+ int[] numConfig = new int[1];
+ assertThat(EGL14.eglChooseConfig(display, new int[0], 0, configs, 0, 0, numConfig, 0)).isTrue();
+ assertThat(numConfig[0]).isGreaterThan(0);
+ }
+
+ @Test
public void eglCreateContext_v2() {
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
int[] attribList = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java
index c0ff1622f..25c7ed722 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java
@@ -18,6 +18,7 @@ import android.speech.SpeechRecognizer;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import java.util.ArrayList;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,6 +43,11 @@ public class ShadowSpeechRecognizerTest {
supportCallback = new TestRecognitionSupportCallback();
}
+ @After
+ public void tearDown() {
+ speechRecognizer.destroy();
+ }
+
@Test
public void onErrorCalled() {
startListening();
@@ -118,6 +124,7 @@ public class ShadowSpeechRecognizerTest {
shadowOf(speechRecognizer).triggerOnResults(new Bundle());
speechRecognizer.stopListening();
+ shadowOf(getMainLooper()).idle();
assertNoErrorLogs();
}
@@ -136,6 +143,8 @@ public class ShadowSpeechRecognizerTest {
/** Verify the startlistening flow works when using custom component name. */
@Test
public void startListeningWithCustomComponent() {
+ speechRecognizer.destroy();
+
speechRecognizer =
SpeechRecognizer.createSpeechRecognizer(
ApplicationProvider.getApplicationContext(),
@@ -157,6 +166,7 @@ public class ShadowSpeechRecognizerTest {
shadowOf(getMainLooper()).idle();
assertThat(ShadowSpeechRecognizer.getLatestSpeechRecognizer())
.isSameInstanceAs(newSpeechRecognizer);
+ newSpeechRecognizer.destroy();
}
@Test
@@ -170,6 +180,7 @@ public class ShadowSpeechRecognizerTest {
newSpeechRecognizer.startListening(intent);
shadowOf(getMainLooper()).idle();
assertThat(shadowOf(newSpeechRecognizer).getLastRecognizerIntent()).isEqualTo(intent);
+ newSpeechRecognizer.destroy();
}
private void startListening() {
@@ -236,6 +247,7 @@ public class ShadowSpeechRecognizerTest {
@Config(minSdk = TIRAMISU)
@Test
public void onCreateOnDeviceRecognizer_setsLatestSpeechRecognizer() {
+ speechRecognizer.destroy();
speechRecognizer = SpeechRecognizer.createOnDeviceSpeechRecognizer(applicationContext);
assertThat(speechRecognizer)
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
index c3535c043..ead36aeea 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
@@ -1530,4 +1530,21 @@ public class ShadowTelephonyManagerTest {
IllegalStateException.class,
() -> shadowTelephonyManager.setCarrierRestrictionRules(new Object()));
}
+
+ @Test
+ @Config(minSdk = Q)
+ public void rebootModem_rebootsModem() {
+ shadowOf((Application) ApplicationProvider.getApplicationContext())
+ .grantPermissions(permission.MODIFY_PHONE_STATE);
+
+ shadowTelephonyManager.rebootModem();
+
+ assertThat(shadowTelephonyManager.getModemRebootCount()).isEqualTo(1);
+ }
+
+ @Test()
+ @Config(minSdk = Q)
+ public void rebootModem_noModifyPhoneStatePermission_throwsSecurityException() {
+ assertThrows(SecurityException.class, () -> shadowTelephonyManager.rebootModem());
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/HardwareRenderingScreenshot.java b/shadows/framework/src/main/java/org/robolectric/shadows/HardwareRenderingScreenshot.java
index 63e9d7bd8..4bb433f90 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/HardwareRenderingScreenshot.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/HardwareRenderingScreenshot.java
@@ -15,7 +15,10 @@ import android.os.Build.VERSION_CODES;
import android.util.DisplayMetrics;
import android.view.Surface;
import android.view.View;
+import android.view.ViewRootImpl;
import com.android.internal.R;
+import com.google.common.base.Preconditions;
+import java.util.WeakHashMap;
import org.robolectric.annotation.GraphicsMode;
import org.robolectric.util.ReflectionHelpers;
@@ -25,6 +28,11 @@ import org.robolectric.util.ReflectionHelpers;
*/
public final class HardwareRenderingScreenshot {
+ // It is important to reuse HardwareRenderer objects, and ensure that after a HardwareRenderer is
+ // collected, no associated views in the same View hierarchy will be rendered as well.
+ private static final WeakHashMap<ViewRootImpl, HardwareRenderer> hardwareRenderers =
+ new WeakHashMap<>();
+
static final String PIXEL_COPY_RENDER_MODE = "robolectric.pixelCopyRenderMode";
private HardwareRenderingScreenshot() {}
@@ -58,8 +66,10 @@ public final class HardwareRenderingScreenshot {
// - ImageReader is configured as RGBA_8888.
// - However the native libs/hwui/pipeline/skia/SkiaHostPipeline.cpp always treats
// the buffer as BGRA_8888, thus matching what the Android Bitmap object requires.
-
- HardwareRenderer renderer = new HardwareRenderer();
+ ViewRootImpl viewRootImpl = view.getViewRootImpl();
+ Preconditions.checkNotNull(viewRootImpl, "View not attached");
+ HardwareRenderer renderer =
+ hardwareRenderers.computeIfAbsent(viewRootImpl, k -> new HardwareRenderer());
Surface surface = imageReader.getSurface();
renderer.setSurface(surface);
Image nativeImage = imageReader.acquireNextImage();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothLeAdvertiser.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothLeAdvertiser.java
index c4e614dd5..5853071dd 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothLeAdvertiser.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothLeAdvertiser.java
@@ -4,11 +4,13 @@ import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothUuid;
+import android.bluetooth.IBluetoothGatt;
import android.bluetooth.IBluetoothManager;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
@@ -26,14 +28,16 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
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;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.util.reflector.Constructor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
+import org.robolectric.versioning.AndroidVersions.V;
/** Shadow implementation of {@link BluetoothLeAdvertiser}. */
@Implements(value = BluetoothLeAdvertiser.class, minSdk = O)
@@ -231,16 +235,28 @@ public class ShadowBluetoothLeAdvertiser {
return;
}
- AdvertisingSet advertisingSet =
- ReflectionHelpers.callConstructor(
- AdvertisingSet.class,
- ClassParameter.from(int.class, advertiserId.getAndAdd(1)),
- ClassParameter.from(
- IBluetoothManager.class,
- ReflectionHelpers.createNullProxy(IBluetoothManager.class)),
- ClassParameter.from(
- AttributionSource.class,
- ReflectionHelpers.callInstanceMethod(bluetoothAdapter, "getAttributionSource")));
+ AdvertisingSet advertisingSet;
+ if (RuntimeEnvironment.getApiLevel() >= V.SDK_INT) {
+ IBluetoothGatt gatt =
+ ReflectionHelpers.callInstanceMethod(bluetoothAdapter, "getBluetoothGatt");
+
+ advertisingSet =
+ reflector(AdvertisingSetReflector.class)
+ .__constructor__(
+ gatt == null ? ReflectionHelpers.createNullProxy(IBluetoothGatt.class) : gatt,
+ advertiserId.getAndAdd(1),
+ bluetoothAdapter,
+ bluetoothAdapter.getAttributionSource());
+ } else {
+ advertisingSet =
+ reflector(AdvertisingSetReflector.class)
+ .__constructor__(
+ advertiserId.getAndAdd(1),
+ ReflectionHelpers.createNullProxy(IBluetoothManager.class),
+ (AttributionSource)
+ ReflectionHelpers.callInstanceMethod(
+ bluetoothAdapter, "getAttributionSource"));
+ }
callback.onAdvertisingSetStarted(
advertisingSet, parameters.getTxPowerLevel(), AdvertisingSetCallback.ADVERTISE_SUCCESS);
@@ -343,4 +359,18 @@ public class ShadowBluetoothLeAdvertiser {
@Direct
void __constructor__(BluetoothAdapter bluetoothAdapter);
}
+
+ @ForType(AdvertisingSet.class)
+ interface AdvertisingSetReflector {
+ @Constructor
+ AdvertisingSet __constructor__(
+ IBluetoothGatt bluetoothGatt,
+ int advertiserId,
+ BluetoothAdapter bluetoothAdapter,
+ AttributionSource attributionSource);
+
+ @Constructor
+ AdvertisingSet __constructor__(
+ int advertiserId, IBluetoothManager bluetoothManager, AttributionSource attributionSource);
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCaptioningManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCaptioningManager.java
index 19762d193..203e43968 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCaptioningManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCaptioningManager.java
@@ -1,72 +1,48 @@
package org.robolectric.shadows;
-import android.annotation.NonNull;
-import android.util.ArraySet;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
import android.view.accessibility.CaptioningManager;
-import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
-import java.util.Locale;
-import javax.annotation.Nullable;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
/** Shadow of {@link android.view.accessibility.CaptioningManager}. */
@Implements(CaptioningManager.class)
public class ShadowCaptioningManager {
- private float fontScale = 1;
- private boolean isEnabled = false;
- @Nullable private Locale locale;
-
- private final ArraySet<CaptioningChangeListener> listeners = new ArraySet<>();
-
- /** Returns 1.0 as default or the most recent value passed to {@link #setFontScale()} */
- @Implementation(minSdk = 19)
- protected float getFontScale() {
- return fontScale;
- }
-
- /** Sets the value to be returned by {@link CaptioningManager#getFontScale()} */
- public void setFontScale(float fontScale) {
- this.fontScale = fontScale;
+ @RealObject private CaptioningManager realCaptioningManager;
- for (CaptioningChangeListener captioningChangeListener : listeners) {
- captioningChangeListener.onFontScaleChanged(fontScale);
- }
+ @Implementation(minSdk = TIRAMISU)
+ protected void setSystemAudioCaptioningEnabled(boolean isEnabled) {
+ Settings.Secure.putInt(
+ getContentResolver(), Settings.Secure.ODI_CAPTIONS_ENABLED, isEnabled ? 1 : 0);
}
- /** Returns false or the most recent value passed to {@link #setEnabled(boolean)} */
- @Implementation(minSdk = 19)
- protected boolean isEnabled() {
- return isEnabled;
+ @Implementation(minSdk = TIRAMISU)
+ protected void setSystemAudioCaptioningUiEnabled(boolean isEnabled) {
+ Settings.Secure.putInt(
+ getContentResolver(), Settings.Secure.ODI_CAPTIONS_VOLUME_UI_ENABLED, isEnabled ? 1 : 0);
}
- /** Sets the value to be returned by {@link CaptioningManager#isEnabled()} */
- public void setEnabled(boolean isEnabled) {
- this.isEnabled = isEnabled;
+ @Implementation(minSdk = TIRAMISU)
+ protected boolean isSystemAudioCaptioningUiEnabled() {
+ return Settings.Secure.getInt(
+ getContentResolver(), Settings.Secure.ODI_CAPTIONS_VOLUME_UI_ENABLED, 1)
+ == 1;
}
- @Implementation(minSdk = 19)
- protected void addCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
- listeners.add(listener);
+ private ContentResolver getContentResolver() {
+ return reflector(CaptioningManagerReflector.class, realCaptioningManager).getContentResolver();
}
- @Implementation(minSdk = 19)
- protected void removeCaptioningChangeListener(@NonNull CaptioningChangeListener listener) {
- listeners.remove(listener);
- }
-
- /** Returns null or the most recent value passed to {@link #setLocale(Locale)} */
- @Implementation(minSdk = 19)
- @Nullable
- protected Locale getLocale() {
- return locale;
- }
-
- /** Sets the value to be returned by {@link CaptioningManager#getLocale()} */
- public void setLocale(@Nullable Locale locale) {
- this.locale = locale;
-
- for (CaptioningChangeListener captioningChangeListener : listeners) {
- captioningChangeListener.onLocaleChanged(locale);
- }
+ @ForType(CaptioningManager.class)
+ interface CaptioningManagerReflector {
+ @Accessor("mContentResolver")
+ ContentResolver getContentResolver();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java
index 4f488b571..547b9f1ff 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowEGL14.java
@@ -37,7 +37,11 @@ public class ShadowEGL14 {
int configSize,
int[] numConfig,
int numConfigOffset) {
- configs[configsOffset] = createEglConfig();
+ // The configs array here can be null, in which case the numConfig output is supposed to be
+ // set to the number of matching configs instead of the number of returned configs.
+ if (configs != null) {
+ configs[configsOffset] = createEglConfig();
+ }
numConfig[numConfigOffset] = 1;
return true;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInCallService.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInCallService.java
index c3b4a3e41..e5997eee9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInCallService.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInCallService.java
@@ -207,9 +207,9 @@ public class ShadowInCallService extends ShadowService {
InCallAdapter adapter = Shadow.newInstanceOf(InCallAdapter.class);
Phone phone;
if (VERSION.SDK_INT > N_MR1) {
- phone = reflector(ReflectorPhone.class, inCallService).newInstance(adapter, "", 0);
+ phone = reflector(ReflectorPhone.class).newInstance(adapter, "", 0);
} else {
- phone = reflector(ReflectorPhone.class, inCallService).newInstance(adapter);
+ phone = reflector(ReflectorPhone.class).newInstance(adapter);
}
ReflectionHelpers.setField(inCallService, "mPhone", phone);
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java
index ec5a63cc4..b7375e557 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java
@@ -11,10 +11,13 @@ import android.content.Intent;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
+import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.speech.IRecognitionService;
import android.speech.RecognitionListener;
+import android.speech.RecognitionSupport;
+import android.speech.RecognitionSupportCallback;
import android.speech.SpeechRecognizer;
import com.google.common.base.Preconditions;
import java.util.Queue;
@@ -35,34 +38,16 @@ import org.robolectric.versioning.AndroidVersions.U;
@Implements(value = SpeechRecognizer.class, looseSignatures = true)
public class ShadowSpeechRecognizer {
- @RealObject SpeechRecognizer realSpeechRecognizer;
- protected static SpeechRecognizer latestSpeechRecognizer;
- private Intent recognizerIntent;
- private RecognitionListener recognitionListener;
- private static boolean isOnDeviceRecognitionAvailable = true;
- private boolean isRecognizerDestroyed = false;
+ @SuppressWarnings("NonFinalStaticField")
+ private static SpeechRecognizer latestSpeechRecognizer;
- private /*RecognitionSupportCallback*/ Object recognitionSupportCallback;
- private Executor recognitionSupportExecutor;
- @Nullable private Intent latestModelDownloadIntent;
-
- /**
- * Returns the latest SpeechRecognizer. This method can only be called after {@link
- * SpeechRecognizer#createSpeechRecognizer(Context)} is called.
- */
- public static SpeechRecognizer getLatestSpeechRecognizer() {
- return latestSpeechRecognizer;
- }
+ @SuppressWarnings("NonFinalStaticField")
+ private static boolean isOnDeviceRecognitionAvailable = true;
- /** Returns the argument passed to the last call to {@link SpeechRecognizer#startListening}. */
- public Intent getLastRecognizerIntent() {
- return recognizerIntent;
- }
+ @RealObject SpeechRecognizer realSpeechRecognizer;
- /** Returns true iff the destroy method of was invoked for the recognizer. */
- public boolean isDestroyed() {
- return isRecognizerDestroyed;
- }
+ // NOTE: Do not manipulate state directly in this class. Call {@link #getState()} instead.
+ private final ShadowSpeechRecognizerState state = new ShadowSpeechRecognizerState();
@Resetter
public static void reset() {
@@ -70,10 +55,12 @@ public class ShadowSpeechRecognizer {
isOnDeviceRecognitionAvailable = true;
}
- @Implementation
- protected void destroy() {
- isRecognizerDestroyed = true;
- reflector(SpeechRecognizerReflector.class, realSpeechRecognizer).destroy();
+ /**
+ * Returns the latest SpeechRecognizer. This method can only be called after {@link
+ * SpeechRecognizer#createSpeechRecognizer(Context)} is called.
+ */
+ public static SpeechRecognizer getLatestSpeechRecognizer() {
+ return latestSpeechRecognizer;
}
@Implementation
@@ -86,145 +73,264 @@ public class ShadowSpeechRecognizer {
return result;
}
- @Implementation
- protected void startListening(Intent recognizerIntent) {
- this.recognizerIntent = recognizerIntent;
- // from the implementation of {@link SpeechRecognizer#startListening} it seems that it allows
- // running the method on an already destroyed object, so we replicate the same by resetting
- // isRecognizerDestroyed
- isRecognizerDestroyed = false;
- // the real implementation connects to a service
- // simulate the resulting behavior once the service is connected
- Handler mainHandler = new Handler(Looper.getMainLooper());
- // perform the onServiceConnected logic
- mainHandler.post(
- () -> {
- SpeechRecognizerReflector recognizerReflector =
- reflector(SpeechRecognizerReflector.class, realSpeechRecognizer);
- recognizerReflector.setService(
- ReflectionHelpers.createNullProxy(IRecognitionService.class));
- Queue<Message> pendingTasks = recognizerReflector.getPendingTasks();
- while (!pendingTasks.isEmpty()) {
- recognizerReflector.getHandler().sendMessage(pendingTasks.poll());
- }
- });
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ protected static SpeechRecognizer createOnDeviceSpeechRecognizer(final Context context) {
+ SpeechRecognizer result =
+ reflector(SpeechRecognizerReflector.class).createOnDeviceSpeechRecognizer(context);
+ latestSpeechRecognizer = result;
+ return result;
+ }
+
+ public static void setIsOnDeviceRecognitionAvailable(boolean available) {
+ isOnDeviceRecognitionAvailable = available;
+ }
+
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ protected static boolean isOnDeviceRecognitionAvailable(final Context context) {
+ return isOnDeviceRecognitionAvailable;
+ }
+
+ /**
+ * Returns the state of this shadow instance.
+ *
+ * <p>Subclasses may override this function to customize which state is returned.
+ */
+ protected ShadowSpeechRecognizerState getState() {
+ return state;
}
/**
- * Handles changing the listener and allows access to the internal listener to trigger events and
- * sets the latest SpeechRecognizer.
+ * Returns the {@link ShadowSpeechRecognizerDirectAccessors} implementation that can handle direct
+ * access to functions/variables of a real {@link SpeechRecognizer}.
+ *
+ * <p>Subclasses may override this function to customize access in case they are shadowing a
+ * subclass of {@link SpeechRecognizer} that functions differently than the parent class.
*/
+ protected ShadowSpeechRecognizerDirectAccessors getDirectAccessors() {
+ return reflector(SpeechRecognizerReflector.class, realSpeechRecognizer);
+ }
+
+ /** Returns true iff the destroy method of was invoked for the recognizer. */
+ public boolean isDestroyed() {
+ return getState().isRecognizerDestroyed;
+ }
+
+ @Implementation(maxSdk = U.SDK_INT)
+ protected void destroy() {
+ getState().isRecognizerDestroyed = true;
+ getDirectAccessors().destroy();
+ }
+
+ /** Returns the argument passed to the last call to {@link SpeechRecognizer#startListening}. */
+ public Intent getLastRecognizerIntent() {
+ return getState().recognizerIntent;
+ }
+
+ @Implementation(maxSdk = U.SDK_INT)
+ protected void startListening(Intent recognizerIntent) {
+ // Record the most recent requested intent.
+ ShadowSpeechRecognizerState shadowState = getState();
+ shadowState.recognizerIntent = recognizerIntent;
+
+ // From the implementation of {@link SpeechRecognizer#startListening} it seems that it allows
+ // running the method on an already destroyed object, so we replicate the same by resetting
+ // isRecognizerDestroyed.
+ shadowState.isRecognizerDestroyed = false;
+
+ // The real implementation connects to a service simulate the resulting behavior once
+ // the service is connected.
+ new Handler(Looper.getMainLooper())
+ .post(
+ () -> {
+ ShadowSpeechRecognizerDirectAccessors directAccessors = getDirectAccessors();
+ directAccessors.setService(createFakeSpeechRecognitionService());
+
+ Handler taskHandler = directAccessors.getHandler();
+ Queue<Message> pendingTasks = directAccessors.getPendingTasks();
+ while (!pendingTasks.isEmpty()) {
+ taskHandler.sendMessage(pendingTasks.poll());
+ }
+ });
+ }
+
+ /** Handles changing the listener and allows access to the internal listener to trigger events. */
@Implementation(maxSdk = U.SDK_INT) // TODO(hoisie): Update this to support Android V
@InDevelopment
protected void handleChangeListener(RecognitionListener listener) {
- recognitionListener = listener;
+ getState().recognitionListener = listener;
}
public void triggerOnEndOfSpeech() {
- recognitionListener.onEndOfSpeech();
+ getState().recognitionListener.onEndOfSpeech();
}
public void triggerOnError(int error) {
- recognitionListener.onError(error);
+ getState().recognitionListener.onError(error);
}
public void triggerOnReadyForSpeech(Bundle bundle) {
- recognitionListener.onReadyForSpeech(bundle);
+ getState().recognitionListener.onReadyForSpeech(bundle);
}
public void triggerOnPartialResults(Bundle bundle) {
- recognitionListener.onPartialResults(bundle);
+ getState().recognitionListener.onPartialResults(bundle);
}
public void triggerOnResults(Bundle bundle) {
- recognitionListener.onResults(bundle);
+ getState().recognitionListener.onResults(bundle);
}
public void triggerOnRmsChanged(float rmsdB) {
- recognitionListener.onRmsChanged(rmsdB);
- }
-
- @Implementation(minSdk = VERSION_CODES.TIRAMISU)
- protected static SpeechRecognizer createOnDeviceSpeechRecognizer(final Context context) {
- SpeechRecognizer result =
- reflector(SpeechRecognizerReflector.class).createOnDeviceSpeechRecognizer(context);
- latestSpeechRecognizer = result;
- return result;
- }
-
- @Implementation(minSdk = VERSION_CODES.TIRAMISU)
- protected static boolean isOnDeviceRecognitionAvailable(final Context context) {
- return isOnDeviceRecognitionAvailable;
+ getState().recognitionListener.onRmsChanged(rmsdB);
}
@RequiresApi(api = VERSION_CODES.TIRAMISU)
- @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU, maxSdk = U.SDK_INT)
protected void checkRecognitionSupport(
@NonNull /*Intent*/ Object recognizerIntent,
@NonNull /*Executor*/ Object executor,
@NonNull /*RecognitionSupportCallback*/ Object supportListener) {
Preconditions.checkArgument(recognizerIntent instanceof Intent);
Preconditions.checkArgument(executor instanceof Executor);
- Preconditions.checkArgument(
- supportListener instanceof android.speech.RecognitionSupportCallback);
- recognitionSupportExecutor = (Executor) executor;
- recognitionSupportCallback = supportListener;
+ Preconditions.checkArgument(supportListener instanceof RecognitionSupportCallback);
+
+ ShadowSpeechRecognizerState shadowState = getState();
+ shadowState.recognitionSupportExecutor = (Executor) executor;
+ shadowState.recognitionSupportCallback = supportListener;
}
- @Implementation(minSdk = VERSION_CODES.TIRAMISU)
- protected void triggerModelDownload(Intent recognizerIntent) {
- latestModelDownloadIntent = recognizerIntent;
+ @RequiresApi(VERSION_CODES.TIRAMISU)
+ @Nullable
+ public Intent getLatestModelDownloadIntent() {
+ return getState().latestModelDownloadIntent;
}
- public static void setIsOnDeviceRecognitionAvailable(boolean available) {
- isOnDeviceRecognitionAvailable = available;
+ @Implementation(minSdk = VERSION_CODES.TIRAMISU, maxSdk = U.SDK_INT)
+ protected void triggerModelDownload(Intent recognizerIntent) {
+ getState().latestModelDownloadIntent = recognizerIntent;
}
@RequiresApi(VERSION_CODES.TIRAMISU)
public void triggerSupportResult(/*RecognitionSupport*/ Object recognitionSupport) {
- Preconditions.checkArgument(recognitionSupport instanceof android.speech.RecognitionSupport);
- recognitionSupportExecutor.execute(
+ Preconditions.checkArgument(recognitionSupport instanceof RecognitionSupport);
+
+ ShadowSpeechRecognizerState shadowState = getState();
+ shadowState.recognitionSupportExecutor.execute(
() ->
- ((android.speech.RecognitionSupportCallback) recognitionSupportCallback)
- .onSupportResult((android.speech.RecognitionSupport) recognitionSupport));
+ ((RecognitionSupportCallback) shadowState.recognitionSupportCallback)
+ .onSupportResult((RecognitionSupport) recognitionSupport));
}
@RequiresApi(VERSION_CODES.TIRAMISU)
public void triggerSupportError(int error) {
- recognitionSupportExecutor.execute(
- () ->
- ((android.speech.RecognitionSupportCallback) recognitionSupportCallback)
- .onError(error));
+ ShadowSpeechRecognizerState shadowState = getState();
+ shadowState.recognitionSupportExecutor.execute(
+ () -> ((RecognitionSupportCallback) shadowState.recognitionSupportCallback).onError(error));
}
- @RequiresApi(VERSION_CODES.TIRAMISU)
- @Nullable
- public Intent getLatestModelDownloadIntent() {
- return latestModelDownloadIntent;
+ /**
+ * {@link SpeechRecognizer} implementation now checks if the service's binder is alive whenever
+ * {@link SpeechRecognizer#checkOpenConnection} is called. This means that we need to return a
+ * deeper proxy that returns a delegating proxy that always reports the binder as alive.
+ */
+ private static IRecognitionService createFakeSpeechRecognitionService() {
+ return ReflectionHelpers.createDelegatingProxy(
+ IRecognitionService.class, new AlwaysAliveSpeechRecognitionServiceDelegate());
+ }
+
+ /**
+ * A proxy delegate for {@link IRecognitionService} that always returns a delegating proxy that
+ * returns an {@link AlwaysAliveBinderDelegate} when {@link IRecognitionService#asBinder()} is
+ * called.
+ *
+ * @see #createFakeSpeechRecognitionService() for more details
+ */
+ private static class AlwaysAliveSpeechRecognitionServiceDelegate {
+ public IBinder asBinder() {
+ return ReflectionHelpers.createDelegatingProxy(
+ IBinder.class, new AlwaysAliveBinderDelegate());
+ }
+ }
+
+ /**
+ * A proxy delegate for {@link IBinder} that always returns when {@link IBinder#isBinderAlive()}
+ * is called.
+ *
+ * @see #createFakeSpeechRecognitionService() for more details
+ */
+ private static class AlwaysAliveBinderDelegate {
+ public boolean isBinderAlive() {
+ return true;
+ }
+ }
+
+ /**
+ * The state of a specific instance of {@link ShadowSpeechRecognizer}.
+ *
+ * <p>NOTE: Not stored as variables in the parent class itself since subclasses may need to return
+ * a different instance of this class to operate on.
+ *
+ * <p>NOTE: This class is public since custom shadows may reside in a different package.
+ */
+ public static class ShadowSpeechRecognizerState {
+ private boolean isRecognizerDestroyed = false;
+ private Intent recognizerIntent;
+ private RecognitionListener recognitionListener;
+ private Executor recognitionSupportExecutor;
+ private /*RecognitionSupportCallback*/ Object recognitionSupportCallback;
+ @Nullable private Intent latestModelDownloadIntent;
+ }
+
+ /**
+ * An interface to access direct functions/variables of an instance of {@link SpeechRecognizer}.
+ *
+ * <p>Abstracted to allow subclasses to return customized accessors.
+ */
+ protected interface ShadowSpeechRecognizerDirectAccessors {
+ /**
+ * Invokes {@link SpeechRecognizer#destroy()} on a real instance of {@link SpeechRecognizer}.
+ */
+ void destroy();
+
+ /** Sets the {@link IRecognitionService} used by a real {@link SpeechRecognizer}. */
+ void setService(IRecognitionService service);
+
+ /** Returns a {@link Queue} of pending async tasks of a real {@link SpeechRecognizer}. */
+ Queue<Message> getPendingTasks();
+
+ /**
+ * Returns the {@link Handler} of a real {@link SpeechRecognizer} that it uses to process any
+ * pending async tasks returned by {@link #getPendingTasks()}.
+ */
+ Handler getHandler();
}
/** Reflector interface for {@link SpeechRecognizer}'s internals. */
@ForType(SpeechRecognizer.class)
- interface SpeechRecognizerReflector {
+ interface SpeechRecognizerReflector extends ShadowSpeechRecognizerDirectAccessors {
@Static
@Direct
SpeechRecognizer createSpeechRecognizer(Context context, ComponentName serviceComponent);
+ @Static
@Direct
+ SpeechRecognizer createOnDeviceSpeechRecognizer(Context context);
+
+ @Direct
+ @Override
void destroy();
@Accessor("mService")
+ @Override
void setService(IRecognitionService service);
@Accessor("mPendingTasks")
+ @Override
Queue<Message> getPendingTasks();
@Accessor("mHandler")
+ @Override
Handler getHandler();
-
- @Static
- @Direct
- SpeechRecognizer createOnDeviceSpeechRecognizer(Context context);
}
}
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 b9f9a949f..02aa751c3 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -68,6 +68,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
@@ -180,6 +181,7 @@ public class ShadowTelephonyManager {
private static Map<Integer, List<EmergencyNumber>> emergencyNumbersList;
private static volatile boolean isDataRoamingEnabled;
private /*CarrierRestrictionRules*/ Object carrierRestrictionRules;
+ private final AtomicInteger modemRebootCount = new AtomicInteger();
/**
* Should be {@link TelephonyManager.BootstrapAuthenticationCallback} but this object was
@@ -680,6 +682,12 @@ public class ShadowTelephonyManager {
}
}
+ private void checkModifyPhoneStatePermission() {
+ if (!checkPermission(permission.MODIFY_PHONE_STATE)) {
+ throw new SecurityException();
+ }
+ }
+
static ShadowInstrumentation getShadowInstrumentation() {
ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
return Shadow.extract(activityThread.getInstrumentation());
@@ -1683,4 +1691,15 @@ public class ShadowTelephonyManager {
protected /*CarrierRestrictionRules*/ Object getCarrierRestrictionRules() {
return carrierRestrictionRules;
}
+
+ /** Implementation for {@link TelephonyManager#rebootModem} */
+ @Implementation(minSdk = Build.VERSION_CODES.TIRAMISU)
+ protected void rebootModem() {
+ checkModifyPhoneStatePermission();
+ modemRebootCount.incrementAndGet();
+ }
+
+ public int getModemRebootCount() {
+ return modemRebootCount.get();
+ }
}