aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRex Hoffman <rexhoffman@google.com>2022-12-17 00:07:04 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-12-17 00:07:04 +0000
commitafca64d5446704036f0c8f0657d086440bd6b91c (patch)
tree3508a823b93a48eaaceabdcea6d1e3dcd62c5a00
parent183707e3c7e551130df7fefafe6cacd6c320c179 (diff)
parent58bd83a4a472f6d6cef6ad0ce2e61f1350d084e1 (diff)
downloadrobolectric-afca64d5446704036f0c8f0657d086440bd6b91c.tar.gz
Merge branch 'upstream-google' into upgrade am: 58bd83a4a4
Original change: https://googleplex-android-review.googlesource.com/c/platform/external/robolectric/+/20740694 Change-Id: Ibb316a3abd99299102ff3e1101c2c79908284bac Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.github/workflows/tests.yml12
-rw-r--r--.gitignore4
-rw-r--r--buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy1
-rw-r--r--errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java12
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java2
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java6
-rw-r--r--plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java34
-rw-r--r--plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java31
-rwxr-xr-xplugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java29
-rw-r--r--plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java18
-rw-r--r--plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java12
-rw-r--r--preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java15
-rw-r--r--processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java8
-rw-r--r--resources/src/main/java/org/robolectric/res/android/FileMap.java61
-rw-r--r--resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java32
-rw-r--r--robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java16
-rw-r--r--robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java8
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ResponderLocationBuilderTest.java93
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java6
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java12
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java850
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java15
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java236
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java36
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java45
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java450
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java15
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java88
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java171
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java72
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowLayoutAnimationControllerTest.java31
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java17
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java28
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java7
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java137
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java24
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java63
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java34
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java12
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java2
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java15
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java55
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java147
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java15
-rw-r--r--sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java4
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java51
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java4
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java42
-rw-r--r--shadows/framework/build.gradle2
-rw-r--r--shadows/framework/src/main/java/android/media/Session2Token.java10
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java8
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ResponderLocationBuilder.java211
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbstractCursor.java23
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java800
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java20
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java47
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java117
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java835
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java4
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java12
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java19
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java18
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java219
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java23
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamcorderProfile.java66
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java641
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java12
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java10
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java40
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java29
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java95
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java77
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLayoutAnimationController.java24
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java922
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCanvas.java642
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java662
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyPath.java558
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java273
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java646
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java23
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java10
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java (renamed from shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java)9
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java62
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowOutline.java15
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java559
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java22
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java8
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java2
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java43
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java32
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java23
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java23
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java40
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java16
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java20
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java268
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java48
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java84
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java52
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java27
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java292
110 files changed, 7791 insertions, 4061 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b6f85759e..c6d887ee4 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -10,6 +10,9 @@ on:
permissions:
contents: read
+env:
+ cache-version: v1
+
jobs:
build:
runs-on: ubuntu-20.04
@@ -99,7 +102,7 @@ jobs:
path: '**/build/test-results/**/TEST-*.xml'
instrumentation-tests:
- runs-on: macos-11
+ runs-on: macos-12
timeout-minutes: 60
needs: build
@@ -135,7 +138,7 @@ jobs:
path: |
~/.android/avd/*
~/.android/adb*
- key: avd-${{ matrix.api-level }}
+ key: avd-${{ matrix.api-level }}-${{ env.cache-version }}
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
@@ -156,6 +159,11 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ steps.determine-target.outputs.TARGET }}
arch: x86_64
+ force-avd-creation: false
+ emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ disable-animations: true
+ disable-spellchecker: true
+
profile: Nexus One
script: |
./gradlew cAT || ./gradlew cAT || ./gradlew cAT || exit 1
diff --git a/.gitignore b/.gitignore
index 03ef5e0a3..cae6b8035 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,9 @@ release.properties
.gradle/
build
+# Android Profiling
+*.hprof
+
# IntelliJ
.idea
*.iml
@@ -40,7 +43,6 @@ classes
tmp
local.properties
-
# CTS stuff
cts/
cts-libs/
diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy
index 7289d0c14..c170f3058 100644
--- a/buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/robolectric/gradle/GradleManagedDevicePlugin.groovy
@@ -8,6 +8,7 @@ class GradleManagedDevicePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.android.testOptions {
+ animationsDisabled = true
devices {
// ./gradlew -Pandroid.sdk.channel=3 nexusOneApi29DebugAndroidTest
nexusOneApi29(ManagedVirtualDevice) {
diff --git a/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java b/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java
index b5aceb4cc..06c634f1c 100644
--- a/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java
+++ b/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java
@@ -20,10 +20,12 @@ import com.sun.source.doctree.StartElementTree;
import com.sun.source.doctree.TextTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
+import com.sun.source.util.DocSourcePositions;
import com.sun.source.util.DocTreePath;
import com.sun.source.util.DocTreePathScanner;
import com.sun.source.util.TreePathScanner;
@@ -31,7 +33,6 @@ import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.DCTree.DCDocComment;
import com.sun.tools.javac.tree.DCTree.DCReference;
-import com.sun.tools.javac.tree.DCTree.DCStartElement;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import java.util.ArrayList;
@@ -113,11 +114,12 @@ public final class RobolectricShadow extends BugChecker implements ClassTreeMatc
@Override
public Void visitStartElement(StartElementTree startElementTree, Void aVoid) {
if (startElementTree.getName().toString().equalsIgnoreCase("p")) {
- DCStartElement node = (DCStartElement) startElementTree;
-
DocTreePath path = getCurrentPath();
- int start = (int) node.getSourcePosition((DCDocComment) path.getDocComment()) + node.pos;
- int end = node.getEndPos((DCDocComment) getCurrentPath().getDocComment());
+ DCDocComment doc = (DCDocComment) path.getDocComment();
+ DocSourcePositions positions = trees.getSourcePositions();
+ CompilationUnitTree compilationUnitTree = path.getTreePath().getCompilationUnit();
+ int start = (int) positions.getStartPosition(compilationUnitTree, doc, startElementTree);
+ int end = (int) positions.getEndPosition(compilationUnitTree, doc, startElementTree);
fixes.add(Optional.of(SuggestedFix.replace(start, end, "")));
}
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java b/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
index fddac6f54..319b873a8 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/graphics/BitmapTest.java
@@ -11,6 +11,7 @@ import static android.os.Build.VERSION_CODES.Q;
import static androidx.test.InstrumentationRegistry.getTargetContext;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
import android.content.res.Resources;
import android.graphics.Bitmap.CompressFormat;
@@ -50,6 +51,7 @@ public class BitmapTest {
@Config(minSdk = P)
@SdkSuppress(minSdkVersion = P)
@Test public void createBitmap() {
+ assumeFalse(Boolean.getBoolean("robolectric.nativeruntime.enableGraphics"));
// Bitmap.createBitmap(Picture) requires hardware-backed bitmaps
HardwareRendererCompat.setDrawingEnabled(true);
Picture picture = new Picture();
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java b/integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java
index 651e2c469..334356bd3 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java
@@ -60,13 +60,15 @@ public class DateFormatTest {
@Test
public void getTimeFormat_am() {
+ // allow both regular and thin whitespace separators
assertThat(DateFormat.getTimeFormat(getApplicationContext()).format(dateAM))
- .isEqualTo("8:24 AM");
+ .matches("8:24\\sAM");
}
@Test
public void getTimeFormat_pm() {
+ // allow both regular and thin whitespace separators
assertThat(DateFormat.getTimeFormat(getApplicationContext()).format(datePM))
- .isEqualTo("4:24 PM");
+ .matches("4:24\\sPM");
}
}
diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java
index 527ee33a9..0c8d0697c 100644
--- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java
+++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/MavenRoboSettings.java
@@ -7,11 +7,13 @@ package org.robolectric;
*/
@Deprecated
public class MavenRoboSettings {
-
+ private static final int DEFAULT_PROXY_PORT = 0;
private static String mavenRepositoryId;
private static String mavenRepositoryUrl;
private static String mavenRepositoryUserName;
private static String mavenRepositoryPassword;
+ private static String mavenProxyHost = "";
+ private static int mavenProxyPort = DEFAULT_PROXY_PORT;
static {
mavenRepositoryId = System.getProperty("robolectric.dependency.repo.id", "mavenCentral");
@@ -19,6 +21,20 @@ public class MavenRoboSettings {
System.getProperty("robolectric.dependency.repo.url", "https://repo1.maven.org/maven2");
mavenRepositoryUserName = System.getProperty("robolectric.dependency.repo.username");
mavenRepositoryPassword = System.getProperty("robolectric.dependency.repo.password");
+
+ String proxyHost = System.getProperty("robolectric.dependency.proxy.host");
+ if (proxyHost != null && !proxyHost.isEmpty()) {
+ mavenProxyHost = proxyHost;
+ }
+
+ String proxyPort = System.getProperty("robolectric.dependency.proxy.port");
+ if (proxyPort != null && !proxyPort.isEmpty()) {
+ try {
+ mavenProxyPort = Integer.parseInt(proxyPort);
+ } catch (NumberFormatException numberFormatException) {
+ mavenProxyPort = DEFAULT_PROXY_PORT;
+ }
+ }
}
public static String getMavenRepositoryId() {
@@ -52,4 +68,20 @@ public class MavenRoboSettings {
public static void setMavenRepositoryPassword(String mavenRepositoryPassword) {
MavenRoboSettings.mavenRepositoryPassword = mavenRepositoryPassword;
}
+
+ public static String getMavenProxyHost() {
+ return mavenProxyHost;
+ }
+
+ public static void setMavenProxyHost(String mavenProxyHost) {
+ MavenRoboSettings.mavenProxyHost = mavenProxyHost;
+ }
+
+ public static int getMavenProxyPort() {
+ return mavenProxyPort;
+ }
+
+ public static void setMavenProxyPort(int mavenProxyPort) {
+ MavenRoboSettings.mavenProxyPort = mavenProxyPort;
+ }
}
diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
index 9b3a28a32..60f852dbc 100644
--- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
+++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java
@@ -14,7 +14,9 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.InetSocketAddress;
import java.net.MalformedURLException;
+import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
@@ -34,6 +36,8 @@ public class MavenArtifactFetcher {
private final String repositoryUrl;
private final String repositoryUserName;
private final String repositoryPassword;
+ private final String proxyHost;
+ private final int proxyPort;
private final File localRepositoryDir;
private final ExecutorService executorService;
private File stagingRepositoryDir;
@@ -42,11 +46,15 @@ public class MavenArtifactFetcher {
String repositoryUrl,
String repositoryUserName,
String repositoryPassword,
+ String proxyHost,
+ int proxyPort,
File localRepositoryDir,
ExecutorService executorService) {
this.repositoryUrl = repositoryUrl;
this.repositoryUserName = repositoryUserName;
this.repositoryPassword = repositoryPassword;
+ this.proxyHost = proxyHost;
+ this.proxyPort = proxyPort;
this.localRepositoryDir = localRepositoryDir;
this.executorService = executorService;
}
@@ -152,7 +160,8 @@ public class MavenArtifactFetcher {
protected ListenableFuture<Void> createFetchToFileTask(URL remoteUrl, File tempFile) {
return Futures.submitAsync(
- new FetchToFileTask(remoteUrl, tempFile, repositoryUserName, repositoryPassword),
+ new FetchToFileTask(
+ remoteUrl, tempFile, repositoryUserName, repositoryPassword, proxyHost, proxyPort),
this.executorService);
}
@@ -168,18 +177,34 @@ public class MavenArtifactFetcher {
private final File localFile;
private String repositoryUserName;
private String repositoryPassword;
+ private String proxyHost;
+ private int proxyPort;
public FetchToFileTask(
- URL remoteURL, File localFile, String repositoryUserName, String repositoryPassword) {
+ URL remoteURL,
+ File localFile,
+ String repositoryUserName,
+ String repositoryPassword,
+ String proxyHost,
+ int proxyPort) {
this.remoteURL = remoteURL;
this.localFile = localFile;
this.repositoryUserName = repositoryUserName;
this.repositoryPassword = repositoryPassword;
+ this.proxyHost = proxyHost;
+ this.proxyPort = proxyPort;
}
@Override
public ListenableFuture<Void> call() throws Exception {
- URLConnection connection = remoteURL.openConnection();
+ URLConnection connection;
+ if (this.proxyHost != null && !this.proxyHost.isEmpty() && this.proxyPort > 0) {
+ Proxy proxy =
+ new Proxy(Proxy.Type.HTTP, new InetSocketAddress(this.proxyHost, this.proxyPort));
+ connection = remoteURL.openConnection(proxy);
+ } else {
+ connection = remoteURL.openConnection();
+ }
// Add authorization header if applicable.
if (!Strings.isNullOrEmpty(this.repositoryUserName)) {
String encoded =
diff --git a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
index 22adfaeb3..bb5604d80 100755
--- a/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
+++ b/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenDependencyResolver.java
@@ -44,11 +44,22 @@ public class MavenDependencyResolver implements DependencyResolver {
private final File localRepositoryDir;
public MavenDependencyResolver() {
- this(MavenRoboSettings.getMavenRepositoryUrl(), MavenRoboSettings.getMavenRepositoryId(), MavenRoboSettings
- .getMavenRepositoryUserName(), MavenRoboSettings.getMavenRepositoryPassword());
+ this(
+ MavenRoboSettings.getMavenRepositoryUrl(),
+ MavenRoboSettings.getMavenRepositoryId(),
+ MavenRoboSettings.getMavenRepositoryUserName(),
+ MavenRoboSettings.getMavenRepositoryPassword(),
+ MavenRoboSettings.getMavenProxyHost(),
+ MavenRoboSettings.getMavenProxyPort());
}
- public MavenDependencyResolver(String repositoryUrl, String repositoryId, String repositoryUserName, String repositoryPassword) {
+ public MavenDependencyResolver(
+ String repositoryUrl,
+ String repositoryId,
+ String repositoryUserName,
+ String repositoryPassword,
+ String proxyHost,
+ int proxyPort) {
this.executorService = createExecutorService();
this.localRepositoryDir = getLocalRepositoryDir();
this.mavenArtifactFetcher =
@@ -56,6 +67,8 @@ public class MavenDependencyResolver implements DependencyResolver {
repositoryUrl,
repositoryUserName,
repositoryPassword,
+ proxyHost,
+ proxyPort,
localRepositoryDir,
this.executorService);
}
@@ -163,10 +176,18 @@ public class MavenDependencyResolver implements DependencyResolver {
String repositoryUrl,
String repositoryUserName,
String repositoryPassword,
+ String proxyHost,
+ int proxyPort,
File localRepositoryDir,
ExecutorService executorService) {
return new MavenArtifactFetcher(
- repositoryUrl, repositoryUserName, repositoryPassword, localRepositoryDir, executorService);
+ repositoryUrl,
+ repositoryUserName,
+ repositoryPassword,
+ proxyHost,
+ proxyPort,
+ localRepositoryDir,
+ executorService);
}
protected ExecutorService createExecutorService() {
diff --git a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java
index 164203b9e..8924257d1 100644
--- a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java
+++ b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java
@@ -15,6 +15,8 @@ public class MavenRoboSettingsTest {
private String originalMavenRepositoryUrl;
private String originalMavenRepositoryUserName;
private String originalMavenRepositoryPassword;
+ private String originalMavenRepositoryProxyHost;
+ private int originalMavenProxyPort;
@Before
public void setUp() {
@@ -22,6 +24,8 @@ public class MavenRoboSettingsTest {
originalMavenRepositoryUrl = MavenRoboSettings.getMavenRepositoryUrl();
originalMavenRepositoryUserName = MavenRoboSettings.getMavenRepositoryUserName();
originalMavenRepositoryPassword = MavenRoboSettings.getMavenRepositoryPassword();
+ originalMavenRepositoryProxyHost = MavenRoboSettings.getMavenProxyHost();
+ originalMavenProxyPort = MavenRoboSettings.getMavenProxyPort();
}
@After
@@ -30,6 +34,8 @@ public class MavenRoboSettingsTest {
MavenRoboSettings.setMavenRepositoryUrl(originalMavenRepositoryUrl);
MavenRoboSettings.setMavenRepositoryUserName(originalMavenRepositoryUserName);
MavenRoboSettings.setMavenRepositoryPassword(originalMavenRepositoryPassword);
+ MavenRoboSettings.setMavenProxyHost(originalMavenRepositoryProxyHost);
+ MavenRoboSettings.setMavenProxyPort(originalMavenProxyPort);
}
@Test
@@ -65,4 +71,16 @@ public class MavenRoboSettingsTest {
MavenRoboSettings.setMavenRepositoryPassword("password");
assertEquals("password", MavenRoboSettings.getMavenRepositoryPassword());
}
+
+ @Test
+ public void setMavenProxyHost() {
+ MavenRoboSettings.setMavenProxyHost("123.4.5.678");
+ assertEquals("123.4.5.678", MavenRoboSettings.getMavenProxyHost());
+ }
+
+ @Test
+ public void setMavenProxyPort() {
+ MavenRoboSettings.setMavenProxyPort(9000);
+ assertEquals(9000, MavenRoboSettings.getMavenProxyPort());
+ }
}
diff --git a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java
index 3849c03e9..f438414d3 100644
--- a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java
+++ b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java
@@ -27,6 +27,8 @@ public class MavenDependencyResolverTest {
private static final String REPOSITORY_URL;
private static final String REPOSITORY_USERNAME = "username";
private static final String REPOSITORY_PASSWORD = "password";
+ private static final String PROXY_HOST = "123.4.5.678";
+ private static final int PROXY_PORT = 9000;
private static final HashFunction SHA512 = Hashing.sha512();
private static DependencyJar[] successCases =
@@ -65,6 +67,8 @@ public class MavenDependencyResolverTest {
REPOSITORY_URL,
REPOSITORY_USERNAME,
REPOSITORY_PASSWORD,
+ PROXY_HOST,
+ PROXY_PORT,
localRepositoryDir,
executorService);
mavenDependencyResolver = new TestMavenDependencyResolver();
@@ -167,6 +171,8 @@ public class MavenDependencyResolverTest {
String repositoryUrl,
String repositoryUserName,
String repositoryPassword,
+ String proxyHost,
+ int proxyPort,
File localRepositoryDir,
ExecutorService executorService) {
return mavenArtifactFetcher;
@@ -200,12 +206,16 @@ public class MavenDependencyResolverTest {
String repositoryUrl,
String repositoryUserName,
String repositoryPassword,
+ String proxyHost,
+ int proxyPort,
File localRepositoryDir,
ExecutorService executorService) {
super(
repositoryUrl,
repositoryUserName,
repositoryPassword,
+ proxyHost,
+ proxyPort,
localRepositoryDir,
executorService);
this.executorService = executorService;
@@ -214,7 +224,7 @@ public class MavenDependencyResolverTest {
@Override
protected ListenableFuture<Void> createFetchToFileTask(URL remoteUrl, File tempFile) {
return Futures.submitAsync(
- new FetchToFileTask(remoteUrl, tempFile, null, null) {
+ new FetchToFileTask(remoteUrl, tempFile, null, null, null, 0) {
@Override
public ListenableFuture<Void> call() throws Exception {
numRequests += 1;
diff --git a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
index ed5769215..c23798eaa 100644
--- a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
+++ b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
@@ -8,6 +8,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Locale;
+import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
@@ -64,6 +65,13 @@ public class JarInstrumentor {
int nonClassCount = 0;
int classCount = 0;
+ // get the jar's SDK version
+ try {
+ classInstrumentor.setAndroidJarSDKVersion(getJarAndroidSDKVersion(jarFile));
+ } catch (Exception e) {
+ throw new AssertionError("Unable to get Android SDK version from Jar File", e);
+ }
+
try (JarOutputStream jarOut =
new JarOutputStream(new BufferedOutputStream(new FileOutputStream(destFile), ONE_MB))) {
Enumeration<JarEntry> entries = jarFile.entries();
@@ -136,4 +144,11 @@ public class JarInstrumentor {
entry.setTime(original.getTime());
return entry;
}
+
+ private int getJarAndroidSDKVersion(JarFile jarFile) throws IOException {
+ ZipEntry buildProp = jarFile.getEntry("build.prop");
+ Properties buildProps = new Properties();
+ buildProps.load(jarFile.getInputStream(buildProp));
+ return Integer.parseInt(buildProps.getProperty("ro.build.version.sdk"));
+ }
}
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 79c246132..fd5e77dd2 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
@@ -4,6 +4,7 @@ import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.newTreeMap;
import static com.google.common.collect.Sets.newTreeSet;
+import com.google.auto.common.MoreTypes;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimaps;
@@ -99,10 +100,9 @@ public class RobolectricModel {
TypeElement shadowBaseType = null;
if (shadowPickerType != null) {
TypeMirror iface = helpers.findInterface(shadowPickerType, ShadowPicker.class);
- if (iface != null) {
- com.sun.tools.javac.code.Type type = ((com.sun.tools.javac.code.Type.ClassType) iface)
- .allparams().get(0);
- String baseClassName = type.asElement().getQualifiedName().toString();
+ if (iface instanceof DeclaredType) {
+ TypeMirror first = MoreTypes.asDeclared(iface).getTypeArguments().get(0);
+ String baseClassName = first.toString();
shadowBaseType = helpers.getTypeElement(baseClassName);
}
}
diff --git a/resources/src/main/java/org/robolectric/res/android/FileMap.java b/resources/src/main/java/org/robolectric/res/android/FileMap.java
index f12726865..0672bbde4 100644
--- a/resources/src/main/java/org/robolectric/res/android/FileMap.java
+++ b/resources/src/main/java/org/robolectric/res/android/FileMap.java
@@ -7,6 +7,7 @@ import static org.robolectric.res.android.Util.ALOGV;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
import com.google.common.primitives.Shorts;
import java.io.File;
import java.io.FileInputStream;
@@ -23,11 +24,20 @@ public class FileMap {
/** ZIP archive central directory end header signature. */
private static final int ENDSIG = 0x6054b50;
- private static final int ENDHDR = 22;
+ private static final int EOCD_SIZE = 22;
+
+ private static final int ZIP64_EOCD_SIZE = 56;
+
+ private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
+
/** ZIP64 archive central directory end header signature. */
private static final int ENDSIG64 = 0x6064b50;
- /** the maximum size of the end of central directory section in bytes */
- private static final int MAXIMUM_ZIP_EOCD_SIZE = 64 * 1024 + ENDHDR;
+
+ private static final int MAX_COMMENT_SIZE = 64 * 1024; // 64k
+
+ /** the maximum size of the end of central directory sections in bytes */
+ private static final int MAXIMUM_ZIP_EOCD_SIZE =
+ MAX_COMMENT_SIZE + EOCD_SIZE + ZIP64_EOCD_SIZE + ZIP64_EOCD_LOCATOR_SIZE;
private ZipFile zipFile;
private ZipEntry zipEntry;
@@ -209,7 +219,6 @@ public class FileMap {
// First read the 'end of central directory record' in order to find the start of the central
// directory
- // The end of central directory record (EOCD) is max comment length (64K) + 22 bytes
int endOfCdSize = Math.min(MAXIMUM_ZIP_EOCD_SIZE, length);
int endofCdOffset = length - endOfCdSize;
randomAccessFile.seek(endofCdOffset);
@@ -217,7 +226,11 @@ public class FileMap {
randomAccessFile.readFully(buffer);
int centralDirOffset = findCentralDir(buffer);
-
+ if (centralDirOffset == -1) {
+ // If the zip file contains > 2^16 entries, a Zip64 EOCD is written, and the central
+ // dir offset in the regular EOCD may be -1.
+ centralDirOffset = findCentralDir64(buffer);
+ }
int offset = centralDirOffset - endofCdOffset;
if (offset < 0) {
// read the entire central directory record into memory
@@ -284,7 +297,7 @@ public class FileMap {
private static int findCentralDir(byte[] buffer) throws IOException {
// find start of central directory by scanning backwards
- int scanOffset = buffer.length - ENDHDR;
+ int scanOffset = buffer.length - EOCD_SIZE;
while (true) {
int val = readInt(buffer, scanOffset);
@@ -305,12 +318,48 @@ public class FileMap {
return offsetToCentralDir;
}
+ private static int findCentralDir64(byte[] buffer) throws IOException {
+ // find start of central directory by scanning backwards
+ int scanOffset = buffer.length - EOCD_SIZE - ZIP64_EOCD_LOCATOR_SIZE - ZIP64_EOCD_SIZE;
+
+ while (true) {
+ int val = readInt(buffer, scanOffset);
+ if (val == ENDSIG64) {
+ break;
+ }
+
+ // Ok, keep backing up looking for the ZIP end central directory
+ // signature.
+ --scanOffset;
+ if (scanOffset < 0) {
+ throw new ZipException("ZIP directory not found, not a ZIP archive.");
+ }
+ }
+ // scanOffset is now start of end of central directory record
+ // the 'offset to central dir' data is at position 16 in the record
+ long offsetToCentralDir = readLong(buffer, scanOffset + 48);
+ return (int) offsetToCentralDir;
+ }
+
/** Read a 32-bit integer from a bytebuffer in little-endian order. */
private static int readInt(byte[] buffer, int offset) {
return Ints.fromBytes(
buffer[offset + 3], buffer[offset + 2], buffer[offset + 1], buffer[offset]);
}
+ /** Read a 64-bit integer from a bytebuffer in little-endian order. */
+ private static long readLong(byte[] buffer, int offset) {
+ return Longs.fromBytes(
+ buffer[offset + 7],
+ buffer[offset + 6],
+ buffer[offset + 5],
+ buffer[offset + 4],
+ buffer[offset + 3],
+ buffer[offset + 2],
+ buffer[offset + 1],
+ buffer[offset]);
+ }
+
/** Read a 16-bit short from a bytebuffer in little-endian order. */
private static short readShort(byte[] buffer, int offset) {
return Shorts.fromBytes(buffer[offset + 1], buffer[offset]);
diff --git a/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java b/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
index eebf3654b..cf48b2109 100644
--- a/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
+++ b/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
@@ -3,9 +3,12 @@ package org.robolectric.res.android;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
+import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -63,4 +66,33 @@ public final class ZipFileROTest {
ZipFileRO zipFile = ZipFileRO.open(blob.toString());
assertThat(zipFile).isNotNull();
}
+
+ @Test
+ public void testCreateJar() throws Exception {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ ZipOutputStream out = new ZipOutputStream(byteArrayOutputStream);
+ // Write 2^16 + 1 entries, forcing zip64 EOCD to be written.
+ for (int i = 0; i < 65537; i++) {
+ out.putNextEntry(new ZipEntry(Integer.toString(i)));
+ out.closeEntry();
+ }
+ out.close();
+ byte[] zipBytes = byteArrayOutputStream.toByteArray();
+ // Write 0xff for the following fields in the EOCD, which some zip libraries do.
+ // Entries in this disk (2 bytes)
+ // Total Entries (2 byte)
+ // Size of Central Dir (4 bytes)
+ // Offset to Central Dir (4 bytes)
+ // Total: 12 bytes
+ for (int i = 0; i < 12; i++) {
+ zipBytes[zipBytes.length - 3 - i] = (byte) 0xff;
+ }
+ File tmpFile = File.createTempFile("zip64eocd", "zip");
+ Files.write(zipBytes, tmpFile);
+ ZipFileRO zro = ZipFileRO.open(tmpFile.getAbsolutePath());
+ assertThat(zro).isNotNull();
+ assertThat(zro.findEntryByName("0")).isNotNull();
+ assertThat(zro.findEntryByName("65536")).isNotNull();
+ assertThat(zro.findEntryByName("65537")).isNull();
+ }
}
diff --git a/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java b/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
index 1bcc4cb9b..5b0b9e90c 100644
--- a/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
+++ b/robolectric/src/main/java/org/robolectric/junit/rules/ExpectedLogMessagesRule.java
@@ -284,7 +284,7 @@ public final class ExpectedLogMessagesRule implements TestRule {
return type == log.type
&& !(tag != null ? !tag.equals(log.tag) : log.tag != null)
&& !(msg != null ? !msg.equals(log.msg) : log.msg != null)
- && !(msgPattern != null ? !msgPattern.equals(log.msgPattern) : log.msgPattern != null)
+ && !(msgPattern != null ? !isEqual(msgPattern, log.msgPattern) : log.msgPattern != null)
&& !(throwableMatcher != null
? !throwableMatcher.equals(log.throwableMatcher)
: log.throwableMatcher != null);
@@ -292,7 +292,7 @@ public final class ExpectedLogMessagesRule implements TestRule {
@Override
public int hashCode() {
- return Objects.hash(type, tag, msg, msgPattern, throwableMatcher);
+ return Objects.hash(type, tag, msg, hash(msgPattern), throwableMatcher);
}
@Override
@@ -313,5 +313,17 @@ public final class ExpectedLogMessagesRule implements TestRule {
+ throwableStr
+ '}';
}
+
+ /** Returns true if the pattern and flags compiled in a {@link Pattern} were the same. */
+ private static boolean isEqual(Pattern a, Pattern b) {
+ return a != null && b != null
+ ? a.pattern().equals(b.pattern()) && a.flags() == b.flags()
+ : Objects.equals(a, b);
+ }
+
+ /** Returns hash for a {@link Pattern} based on the pattern and flags it was compiled with. */
+ private static int hash(Pattern pattern) {
+ return pattern == null ? 0 : Objects.hash(pattern.pattern(), pattern.flags());
+ }
}
}
diff --git a/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java b/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
index 44e607da5..cd0f30191 100644
--- a/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
+++ b/robolectric/src/test/java/org/robolectric/junit/rules/ExpectedLogMessagesRuleTest.java
@@ -4,6 +4,7 @@ import static org.hamcrest.CoreMatchers.instanceOf;
import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.regex.Pattern;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Rule;
@@ -206,4 +207,11 @@ public final class ExpectedLogMessagesRuleTest {
}
});
}
+
+ @Test
+ public void expectLogMessageWithPattern_duplicatePatterns() {
+ Log.e("Mytag", "message1");
+ rule.expectLogMessagePattern(Log.ERROR, "Mytag", Pattern.compile("message1"));
+ rule.expectLogMessagePattern(Log.ERROR, "Mytag", Pattern.compile("message1"));
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ResponderLocationBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/ResponderLocationBuilderTest.java
new file mode 100644
index 000000000..0478046fd
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ResponderLocationBuilderTest.java
@@ -0,0 +1,93 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import android.net.wifi.rtt.ResponderLocation;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public final class ResponderLocationBuilderTest {
+
+ @Test
+ @Config(minSdk = Q)
+ public void getNewInstance_wouldHaveEmptySubelements() {
+ ResponderLocation responderLocation = ResponderLocationBuilder.newBuilder().build();
+
+ assertThat(responderLocation.isLciSubelementValid()).isFalse();
+ assertThat(responderLocation.isZaxisSubelementValid()).isFalse();
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void settingAllLciSubelementFieldsWithNoZaxisFields() {
+ ResponderLocation responderLocation =
+ ResponderLocationBuilder.newBuilder()
+ .setAltitude(498.9)
+ .setAltitudeUncertainty(2.0)
+ .setLatitude(29.1)
+ .setLatitudeUncertainty(3.4)
+ .setLongitude(87.1)
+ .setLongitudeUncertainty(5.4)
+ .setAltitudeType(ResponderLocation.ALTITUDE_UNDEFINED)
+ .setLciVersion(ResponderLocation.LCI_VERSION_1)
+ .setLciRegisteredLocationAgreement(true)
+ .setDatum(1)
+ .build();
+
+ assertThat(responderLocation.isLciSubelementValid()).isTrue();
+ assertThat(responderLocation.isZaxisSubelementValid()).isFalse();
+ assertThrows(IllegalStateException.class, () -> responderLocation.getFloorNumber());
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void settingPartsOfLciSubelementFields() {
+ ResponderLocation responderLocation =
+ ResponderLocationBuilder.newBuilder()
+ .setAltitude(498.9)
+ .setAltitudeUncertainty(2.0)
+ .setLatitude(29.1)
+ .setLatitudeUncertainty(3.4)
+ .setLongitude(87.1)
+ .setLongitudeUncertainty(5.4)
+ .setLciVersion(ResponderLocation.LCI_VERSION_1)
+ .setLciRegisteredLocationAgreement(true)
+ .setDatum(1)
+ .build();
+
+ assertThat(responderLocation.isLciSubelementValid()).isFalse();
+ assertThat(responderLocation.isZaxisSubelementValid()).isFalse();
+ assertThrows(IllegalStateException.class, () -> responderLocation.getAltitude());
+ assertThrows(IllegalStateException.class, () -> responderLocation.getFloorNumber());
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void settingAllLciSubelementAndZaxisSubelementFields() {
+ ResponderLocation responderLocation =
+ ResponderLocationBuilder.newBuilder()
+ .setAltitude(498.9)
+ .setAltitudeUncertainty(2.0)
+ .setLatitude(29.1)
+ .setLatitudeUncertainty(3.4)
+ .setLongitude(87.1)
+ .setLongitudeUncertainty(5.4)
+ .setAltitudeType(ResponderLocation.ALTITUDE_METERS)
+ .setLciVersion(ResponderLocation.LCI_VERSION_1)
+ .setLciRegisteredLocationAgreement(true)
+ .setDatum(1)
+ .setHeightAboveFloorMeters(2.1)
+ .setHeightAboveFloorUncertaintyMeters(0.1)
+ .setFloorNumber(3.0)
+ .setExpectedToMove(1)
+ .build();
+
+ assertThat(responderLocation.isLciSubelementValid()).isTrue();
+ assertThat(responderLocation.isZaxisSubelementValid()).isTrue();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java
index 100f840c5..e7bdce690 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAbstractCursorTest.java
@@ -12,7 +12,6 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.Shadows;
@RunWith(AndroidJUnit4.class)
public class ShadowAbstractCursorTest {
@@ -212,11 +211,10 @@ public class ShadowAbstractCursorTest {
@Test
public void testGetNotificationUri() {
Uri uri = Uri.parse("content://foo.com");
- ShadowAbstractCursor shadow = Shadows.shadowOf(cursor);
- assertThat(shadow.getNotificationUri_Compatibility()).isNull();
+ assertThat(cursor.getNotificationUri()).isNull();
cursor.setNotificationUri(
ApplicationProvider.getApplicationContext().getContentResolver(), uri);
- assertThat(shadow.getNotificationUri_Compatibility()).isEqualTo(uri);
+ assertThat(cursor.getNotificationUri()).isEqualTo(uri);
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
index 4ac1f2e7d..14c91f404 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
@@ -245,6 +245,18 @@ public class ShadowAccessibilityNodeInfoTest {
}
@Test
+ @Config(minSdk = P)
+ public void clone_preservesPaneTitle() {
+ String title = "pane title";
+ AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
+ node.setPaneTitle(title);
+
+ AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(node);
+
+ assertThat(clone.getPaneTitle().toString()).isEqualTo(title);
+ }
+
+ @Test
public void testGetBoundsInScreen() {
AccessibilityNodeInfo root = AccessibilityNodeInfo.obtain();
Rect expected = new Rect(0, 0, 100, 100);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java
index c8e1152f3..7a6ab89af 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAlarmManagerTest.java
@@ -1,52 +1,50 @@
package org.robolectric.shadows;
-import static android.app.AlarmManager.INTERVAL_HOUR;
-import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
-import static android.os.Build.VERSION_CODES.KITKAT;
-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.S;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
-import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlarmManager.AlarmClockInfo;
import android.app.AlarmManager.OnAlarmListener;
import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.os.Build.VERSION_CODES;
-import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.WorkSource;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import java.util.Date;
+import java.time.Duration;
+import java.util.Objects;
import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAlarmManager.ScheduledAlarm;
@RunWith(AndroidJUnit4.class)
public class ShadowAlarmManagerTest {
private Context context;
- private Activity activity;
private AlarmManager alarmManager;
- private ShadowAlarmManager shadowAlarmManager;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- shadowAlarmManager = shadowOf(alarmManager);
- activity = Robolectric.setupActivity(Activity.class);
- TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
- assertThat(TimeZone.getDefault().getID()).isEqualTo("America/Los_Angeles");
+ ShadowAlarmManager.setAutoSchedule(true);
}
@Test
@@ -64,13 +62,7 @@ public class ShadowAlarmManagerTest {
@Test
@Config(minSdk = VERSION_CODES.M)
public void setTimeZone_abbreviateTimeZone_ignore() {
- try {
- alarmManager.setTimeZone("PST");
- fail("IllegalArgumentException not thrown");
- } catch (IllegalArgumentException e) {
- // expected
- }
- assertThat(TimeZone.getDefault().getID()).isEqualTo("America/Los_Angeles");
+ assertThrows(IllegalArgumentException.class, () -> alarmManager.setTimeZone("PST"));
}
@Test
@@ -83,13 +75,7 @@ public class ShadowAlarmManagerTest {
@Test
@Config(minSdk = VERSION_CODES.M)
public void setTimeZone_invalidTimeZone_ignore() {
- try {
- alarmManager.setTimeZone("-07:00");
- fail("IllegalArgumentException not thrown");
- } catch (IllegalArgumentException e) {
- // expected
- }
- assertThat(TimeZone.getDefault().getID()).isEqualTo("America/Los_Angeles");
+ assertThrows(IllegalArgumentException.class, () -> alarmManager.setTimeZone("-07:00"));
}
@Test
@@ -100,361 +86,623 @@ public class ShadowAlarmManagerTest {
}
@Test
- public void set_shouldRegisterAlarm() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
+ public void set_pendingIntent() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).run();
+ }
+ }
+
+ @Config(minSdk = VERSION_CODES.N)
+ @Test
+ public void set_alarmListener() {
+ OnAlarmListener onFire = mock(OnAlarmListener.class);
alarmManager.set(
- AlarmManager.ELAPSED_REALTIME,
- 0,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
+ AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 10, "tag", onFire, null);
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getTag()).isEqualTo("tag");
- ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
- assertThat(scheduledAlarm).isNotNull();
- assertThat(scheduledAlarm.allowWhileIdle).isFalse();
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).onAlarm();
}
@Test
- @Config(minSdk = N)
- public void set_shouldRegisterAlarm_forApi24() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- OnAlarmListener listener = () -> {};
- alarmManager.set(AlarmManager.ELAPSED_REALTIME, 0, "tag", listener, null);
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
+ public void setRepeating_pendingIntent() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.setRepeating(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ 20L,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getIntervalMs()).isEqualTo(20);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire, times(1)).run();
+
+ alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+ assertThat(alarm.getIntervalMs()).isEqualTo(20);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+ verify(onFire, times(2)).run();
+ }
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+ verify(onFire, times(2)).run();
}
+ @Config(minSdk = VERSION_CODES.KITKAT)
@Test
- @Config(minSdk = M)
- public void setAndAllowWhileIdle_shouldRegisterAlarm() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- alarmManager.setAndAllowWhileIdle(
- AlarmManager.ELAPSED_REALTIME,
- 0,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
-
- ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
- assertThat(scheduledAlarm).isNotNull();
- assertThat(scheduledAlarm.allowWhileIdle).isTrue();
+ public void setWindow_pendingIntent() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.setWindow(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ 20L,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).run();
+ }
}
+ @Config(minSdk = VERSION_CODES.N)
@Test
- @Config(minSdk = M)
- public void setExactAndAllowWhileIdle_shouldRegisterAlarm() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- alarmManager.setExactAndAllowWhileIdle(
+ public void setWindow_alarmListener() {
+ OnAlarmListener onFire = mock(OnAlarmListener.class);
+ alarmManager.setWindow(
AlarmManager.ELAPSED_REALTIME,
- 0,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
+ SystemClock.elapsedRealtime() + 10,
+ 20L,
+ "tag",
+ onFire,
+ null);
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+ assertThat(alarm.getTag()).isEqualTo("tag");
- ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
- assertThat(scheduledAlarm).isNotNull();
- assertThat(scheduledAlarm.allowWhileIdle).isTrue();
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).onAlarm();
}
+ @Config(minSdk = VERSION_CODES.S)
@Test
- @Config(minSdk = KITKAT)
- public void setExact_shouldRegisterAlarm_forApi19() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- alarmManager.setExact(
+ public void setPrioritized_alarmListener() {
+ OnAlarmListener onFire = mock(OnAlarmListener.class);
+ alarmManager.setPrioritized(
AlarmManager.ELAPSED_REALTIME,
- 0,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
- }
+ SystemClock.elapsedRealtime() + 10,
+ 20L,
+ "tag",
+ Runnable::run,
+ onFire);
- @Test
- @Config(minSdk = N)
- public void setExact_shouldRegisterAlarm_forApi124() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- OnAlarmListener listener = () -> {};
- alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, 0, "tag", listener, null);
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+ assertThat(alarm.getTag()).isEqualTo("tag");
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).onAlarm();
}
+ @Config(minSdk = VERSION_CODES.KITKAT)
@Test
- @Config(minSdk = KITKAT)
- public void setWindow_shouldRegisterAlarm_forApi19() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- alarmManager.setWindow(
- AlarmManager.ELAPSED_REALTIME,
- 0,
- 1,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
+ public void setExact_pendingIntent() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.setExact(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).run();
+ }
}
+ @Config(minSdk = VERSION_CODES.N)
@Test
- @Config(minSdk = N)
- public void setWindow_shouldRegisterAlarm_forApi24() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- OnAlarmListener listener = () -> {};
- alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME, 0, 1, "tag", listener, null);
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
+ public void setExact_alarmListener() {
+ OnAlarmListener onFire = mock(OnAlarmListener.class);
+ alarmManager.setExact(
+ AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 10, "tag", onFire, null);
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getTag()).isEqualTo("tag");
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).onAlarm();
}
+ @Config(minSdk = VERSION_CODES.LOLLIPOP)
@Test
- public void setRepeating_shouldRegisterAlarm() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- alarmManager.setRepeating(
- AlarmManager.ELAPSED_REALTIME,
- 0,
- INTERVAL_HOUR,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNotNull();
+ public void setAlarmClock_pendingIntent() {
+ AlarmClockInfo alarmClockInfo =
+ new AlarmClockInfo(
+ SystemClock.elapsedRealtime() + 10,
+ PendingIntent.getBroadcast(context, 0, new Intent("show"), 0));
+
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.setAlarmClock(alarmClockInfo, listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.RTC_WAKEUP);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getAlarmClockInfo()).isEqualTo(alarmClockInfo);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).run();
+ }
}
+ @Config(minSdk = VERSION_CODES.KITKAT)
@Test
- public void set_shouldReplaceAlarmsWithSameIntentReceiver() {
- alarmManager.set(
- AlarmManager.ELAPSED_REALTIME,
- 500,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
- alarmManager.set(
- AlarmManager.ELAPSED_REALTIME,
- 1000,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
+ public void set_pendingIntent_workSource() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ 20L,
+ 0L,
+ listener.getPendingIntent(),
+ new WorkSource());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+ assertThat(alarm.getIntervalMs()).isEqualTo(0);
+ assertThat(alarm.getWorkSource()).isEqualTo(new WorkSource());
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).run();
+ }
}
+ @Config(minSdk = VERSION_CODES.N)
@Test
- public void set_shouldReplaceDuplicates() {
+ public void set_alarmListener_workSource() {
+ OnAlarmListener onFire = mock(OnAlarmListener.class);
alarmManager.set(
AlarmManager.ELAPSED_REALTIME,
- 0,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
+ SystemClock.elapsedRealtime() + 10,
+ 20L,
+ 0L,
+ "tag",
+ onFire,
+ null,
+ new WorkSource());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+ assertThat(alarm.getIntervalMs()).isEqualTo(0);
+ assertThat(alarm.getTag()).isEqualTo("tag");
+ assertThat(alarm.getWorkSource()).isEqualTo(new WorkSource());
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).onAlarm();
+ }
+
+ @Config(minSdk = VERSION_CODES.N)
+ @Test
+ public void set_alarmListener_workSource_noTag() {
+ OnAlarmListener onFire = mock(OnAlarmListener.class);
alarmManager.set(
AlarmManager.ELAPSED_REALTIME,
- 0,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
- }
-
+ SystemClock.elapsedRealtime() + 10,
+ 20L,
+ 0L,
+ onFire,
+ null,
+ new WorkSource());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getWindowLengthMs()).isEqualTo(20);
+ assertThat(alarm.getIntervalMs()).isEqualTo(0);
+ assertThat(alarm.getWorkSource()).isEqualTo(new WorkSource());
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).onAlarm();
+ }
+
+ @Config(minSdk = VERSION_CODES.S)
@Test
- public void setRepeating_shouldReplaceDuplicates() {
- alarmManager.setRepeating(
- AlarmManager.ELAPSED_REALTIME,
- 0,
- INTERVAL_HOUR,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
- alarmManager.setRepeating(
+ public void setExact_alarmListener_workSource() {
+ OnAlarmListener onFire = mock(OnAlarmListener.class);
+ alarmManager.setExact(
AlarmManager.ELAPSED_REALTIME,
- 0,
- INTERVAL_HOUR,
- PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()), 0));
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
- }
-
- @Test
- @SuppressWarnings("JavaUtilDate")
- public void shouldSupportGetNextScheduledAlarm() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
+ SystemClock.elapsedRealtime() + 10,
+ "tag",
+ Runnable::run,
+ new WorkSource(),
+ onFire);
- long now = new Date().getTime();
- Intent intent = new Intent(activity, activity.getClass());
- PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
- alarmManager.set(AlarmManager.ELAPSED_REALTIME, now, pendingIntent);
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getTag()).isEqualTo("tag");
+ assertThat(alarm.getWorkSource()).isEqualTo(new WorkSource());
- ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- assertScheduledAlarm(now, pendingIntent, scheduledAlarm);
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).onAlarm();
}
@Test
- @SuppressWarnings("JavaUtilDate")
- public void getNextScheduledAlarm_shouldReturnRepeatingAlarms() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-
- long now = new Date().getTime();
- Intent intent = new Intent(activity, activity.getClass());
- PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
- alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, now, INTERVAL_HOUR, pendingIntent);
+ public void setInexactRepeating_pendingIntent() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.setInexactRepeating(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ 20L,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getIntervalMs()).isEqualTo(20);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire, times(1)).run();
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+ verify(onFire, times(2)).run();
+ }
- ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.getNextScheduledAlarm();
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
- assertRepeatingScheduledAlarm(now, INTERVAL_HOUR, pendingIntent, scheduledAlarm);
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+ verify(onFire, times(2)).run();
}
+ @Config(minSdk = VERSION_CODES.M)
@Test
- @SuppressWarnings("JavaUtilDate")
- public void peekNextScheduledAlarm_shouldReturnNextAlarm() {
- assertThat(shadowAlarmManager.getNextScheduledAlarm()).isNull();
-
- long now = new Date().getTime();
- Intent intent = new Intent(activity, activity.getClass());
- PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
- alarmManager.set(AlarmManager.ELAPSED_REALTIME, now, pendingIntent);
-
- ShadowAlarmManager.ScheduledAlarm scheduledAlarm = shadowAlarmManager.peekNextScheduledAlarm();
- assertThat(shadowAlarmManager.peekNextScheduledAlarm()).isNotNull();
- assertScheduledAlarm(now, pendingIntent, scheduledAlarm);
+ public void setAndAllowWhileIdle_pendingIntent() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.setAndAllowWhileIdle(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.isAllowWhileIdle()).isTrue();
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire, times(1)).run();
+ }
}
+ @Config(minSdk = VERSION_CODES.M)
@Test
- public void cancel_removesMatchingPendingIntents() {
- Intent intent = new Intent(context, String.class);
- PendingIntent pendingIntent =
- PendingIntent.getBroadcast(context, 0, intent, FLAG_UPDATE_CURRENT);
- alarmManager.set(AlarmManager.RTC, 1337, pendingIntent);
-
- Intent intent2 = new Intent(context, Integer.class);
- PendingIntent pendingIntent2 =
- PendingIntent.getBroadcast(context, 0, intent2, FLAG_UPDATE_CURRENT);
- alarmManager.set(AlarmManager.RTC, 1337, pendingIntent2);
-
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(2);
-
- Intent intent3 = new Intent(context, String.class);
- PendingIntent pendingIntent3 =
- PendingIntent.getBroadcast(context, 0, intent3, FLAG_UPDATE_CURRENT);
- alarmManager.cancel(pendingIntent3);
-
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
+ public void setExactAndAllowWhileIdle_pendingIntent() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.setExactAndAllowWhileIdle(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.isAllowWhileIdle()).isTrue();
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire, times(1)).run();
+ }
}
@Test
- public void cancel_removesMatchingPendingIntentsWithActions() {
- Intent newIntent = new Intent("someAction");
- PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, newIntent, 0);
-
- alarmManager.set(AlarmManager.RTC, 1337, pendingIntent);
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
-
- alarmManager.cancel(PendingIntent.getBroadcast(context, 0, new Intent("anotherAction"), 0));
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
-
- alarmManager.cancel(PendingIntent.getBroadcast(context, 0, new Intent("someAction"), 0));
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(0);
+ public void cancel_pendingIntent() {
+ Runnable onFire1 = mock(Runnable.class);
+ Runnable onFire2 = mock(Runnable.class);
+ try (TestBroadcastListener listener1 =
+ new TestBroadcastListener(onFire1, "action1").register();
+ TestBroadcastListener listener2 =
+ new TestBroadcastListener(onFire2, "action2").register()) {
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 20,
+ listener1.getPendingIntent());
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ listener2.getPendingIntent());
+
+ assertThat(shadowOf(alarmManager).getScheduledAlarms()).hasSize(2);
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+
+ alarmManager.cancel(listener2.getPendingIntent());
+
+ assertThat(shadowOf(alarmManager).getScheduledAlarms()).hasSize(1);
+ alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+
+ alarmManager.cancel(listener1.getPendingIntent());
+
+ assertThat(shadowOf(alarmManager).getScheduledAlarms()).isEmpty();
+ assertThat(shadowOf(alarmManager).peekNextScheduledAlarm()).isNull();
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+ verify(onFire1, never()).run();
+ verify(onFire2, never()).run();
+ }
}
+ @Config(minSdk = VERSION_CODES.N)
@Test
- public void schedule_useRequestCodeToMatchExistingPendingIntents() {
- Intent intent = new Intent("ACTION!");
- PendingIntent pI = PendingIntent.getService(context, 1, intent, 0);
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 10, pI);
+ public void cancel_alarmListener() {
+ OnAlarmListener onFire1 = mock(OnAlarmListener.class);
+ OnAlarmListener onFire2 = mock(OnAlarmListener.class);
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 20, "tag", onFire1, null);
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 10, "tag", onFire2, null);
- PendingIntent pI2 = PendingIntent.getService(context, 2, intent, 0);
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 10, pI2);
+ assertThat(shadowOf(alarmManager).getScheduledAlarms()).hasSize(2);
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(2);
- }
+ alarmManager.cancel(onFire2);
- @Test
- public void cancel_useRequestCodeToMatchExistingPendingIntents() {
- Intent intent = new Intent("ACTION!");
- PendingIntent pI = PendingIntent.getService(context, 1, intent, 0);
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 10, pI);
+ assertThat(shadowOf(alarmManager).getScheduledAlarms()).hasSize(1);
+ alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
- PendingIntent pI2 = PendingIntent.getService(context, 2, intent, 0);
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 10, pI2);
+ alarmManager.cancel(onFire1);
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(2);
+ assertThat(shadowOf(alarmManager).getScheduledAlarms()).isEmpty();
+ assertThat(shadowOf(alarmManager).peekNextScheduledAlarm()).isNull();
- alarmManager.cancel(pI);
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(1);
- assertThat(shadowAlarmManager.getNextScheduledAlarm().operation).isEqualTo(pI2);
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(20));
+ verify(onFire1, never()).onAlarm();
+ verify(onFire2, never()).onAlarm();
}
@Test
- @Config(minSdk = N)
- public void cancel_removesMatchingListeners() {
- Intent intent = new Intent("ACTION!");
- PendingIntent pI = PendingIntent.getService(context, 1, intent, 0);
- OnAlarmListener listener1 = () -> {};
- OnAlarmListener listener2 = () -> {};
- Handler handler = new Handler();
+ @Config(minSdk = VERSION_CODES.S)
+ public void canScheduleExactAlarms() {
+ assertThat(alarmManager.canScheduleExactAlarms()).isFalse();
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 20, "tag", listener1, handler);
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 30, "tag", listener2, handler);
- alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, 40, pI);
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(3);
+ ShadowAlarmManager.setCanScheduleExactAlarms(true);
+ assertThat(alarmManager.canScheduleExactAlarms()).isTrue();
- alarmManager.cancel(listener1);
- assertThat(shadowAlarmManager.getScheduledAlarms()).hasSize(2);
- assertThat(shadowAlarmManager.peekNextScheduledAlarm().onAlarmListener).isEqualTo(listener2);
- assertThat(shadowAlarmManager.peekNextScheduledAlarm().handler).isEqualTo(handler);
+ ShadowAlarmManager.setCanScheduleExactAlarms(false);
+ assertThat(alarmManager.canScheduleExactAlarms()).isFalse();
}
@Test
- @Config(minSdk = LOLLIPOP)
+ @Config(minSdk = VERSION_CODES.LOLLIPOP)
public void getNextAlarmClockInfo() {
+ AlarmClockInfo alarmClockInfo1 =
+ new AlarmClockInfo(
+ SystemClock.elapsedRealtime() + 10,
+ PendingIntent.getBroadcast(context, 0, new Intent("show1"), 0));
+ AlarmClockInfo alarmClockInfo2 =
+ new AlarmClockInfo(
+ SystemClock.elapsedRealtime() + 5,
+ PendingIntent.getBroadcast(context, 0, new Intent("show2"), 0));
+
+ alarmManager.setAlarmClock(
+ alarmClockInfo1, PendingIntent.getBroadcast(context, 0, new Intent("fire1"), 0));
+ alarmManager.setAlarmClock(
+ alarmClockInfo2, PendingIntent.getBroadcast(context, 0, new Intent("fire2"), 0));
+ assertThat(alarmManager.getNextAlarmClock()).isEqualTo(alarmClockInfo2);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(5));
+ assertThat(alarmManager.getNextAlarmClock()).isEqualTo(alarmClockInfo1);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(5));
assertThat(alarmManager.getNextAlarmClock()).isNull();
- assertThat(shadowAlarmManager.peekNextScheduledAlarm()).isNull();
-
- // Schedule an alarm.
- PendingIntent show = PendingIntent.getBroadcast(context, 0, new Intent("showAction"), 0);
- PendingIntent operation = PendingIntent.getBroadcast(context, 0, new Intent("opAction"), 0);
- AlarmClockInfo info = new AlarmClockInfo(1000, show);
- alarmManager.setAlarmClock(info, operation);
-
- AlarmClockInfo next = alarmManager.getNextAlarmClock();
- assertThat(next).isNotNull();
- assertThat(next.getTriggerTime()).isEqualTo(1000);
- assertThat(next.getShowIntent()).isSameInstanceAs(show);
- assertThat(shadowAlarmManager.peekNextScheduledAlarm().operation).isSameInstanceAs(operation);
-
- // Schedule another alarm sooner.
- PendingIntent show2 = PendingIntent.getBroadcast(context, 0, new Intent("showAction2"), 0);
- PendingIntent operation2 = PendingIntent.getBroadcast(context, 0, new Intent("opAction2"), 0);
- AlarmClockInfo info2 = new AlarmClockInfo(500, show2);
- alarmManager.setAlarmClock(info2, operation2);
-
- next = alarmManager.getNextAlarmClock();
- assertThat(next).isNotNull();
- assertThat(next.getTriggerTime()).isEqualTo(500);
- assertThat(next.getShowIntent()).isSameInstanceAs(show2);
- assertThat(shadowAlarmManager.peekNextScheduledAlarm().operation).isSameInstanceAs(operation2);
-
- // Remove the soonest alarm.
- alarmManager.cancel(operation2);
-
- next = alarmManager.getNextAlarmClock();
- assertThat(next).isNotNull();
- assertThat(next.getTriggerTime()).isEqualTo(1000);
- assertThat(next.getShowIntent()).isSameInstanceAs(show);
- assertThat(shadowAlarmManager.peekNextScheduledAlarm().operation).isSameInstanceAs(operation);
-
- // Remove the sole alarm.
- alarmManager.cancel(operation);
-
- assertThat(alarmManager.getNextAlarmClock()).isNull();
- assertThat(shadowAlarmManager.peekNextScheduledAlarm()).isNull();
}
@Test
- @Config(minSdk = S)
- public void canScheduleExactAlarms_default_returnsTrue() {
- assertThat(alarmManager.canScheduleExactAlarms()).isFalse();
+ public void replace_pendingIntent() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ listener.getPendingIntent());
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + 20,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME_WAKEUP);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire, never()).run();
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).run();
+ }
}
+ @Config(minSdk = VERSION_CODES.N)
@Test
- @Config(minSdk = S)
- public void canScheduleExactAlarms_setCanScheduleExactAlarms_returnsTrue() {
- ShadowAlarmManager.setCanScheduleExactAlarms(true);
+ public void replace_alarmListener() {
+ OnAlarmListener onFire = mock(OnAlarmListener.class);
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 10, "tag", onFire, null);
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + 20,
+ "tag1",
+ onFire,
+ null);
- assertThat(alarmManager.canScheduleExactAlarms()).isTrue();
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME_WAKEUP);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 20);
+ assertThat(alarm.getTag()).isEqualTo("tag1");
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire, never()).onAlarm();
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ verify(onFire).onAlarm();
}
@Test
- @Config(minSdk = S)
- public void canScheduleExactAlarms_setCannotScheduleExactAlarms_returnsFalse() {
- ShadowAlarmManager.setCanScheduleExactAlarms(false);
-
- assertThat(alarmManager.canScheduleExactAlarms()).isFalse();
+ public void pastTime() {
+ Runnable onFire = mock(Runnable.class);
+ try (TestBroadcastListener listener = new TestBroadcastListener(onFire, "action").register()) {
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() - 10,
+ listener.getPendingIntent());
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() - 10);
+
+ shadowOf(Looper.getMainLooper()).idle();
+ verify(onFire).run();
+
+ assertThat(shadowOf(alarmManager).peekNextScheduledAlarm()).isNull();
+ }
}
- private void assertScheduledAlarm(
- long now, PendingIntent pendingIntent, ShadowAlarmManager.ScheduledAlarm scheduledAlarm) {
- assertRepeatingScheduledAlarm(now, 0L, pendingIntent, scheduledAlarm);
+ @Config(minSdk = VERSION_CODES.N)
+ @Test
+ public void reentrant() {
+ AtomicReference<OnAlarmListener> listenerRef = new AtomicReference<>();
+ listenerRef.set(
+ () ->
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME,
+ SystemClock.elapsedRealtime() + 10,
+ "tag",
+ listenerRef.get(),
+ null));
+ alarmManager.set(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + 10,
+ "tag",
+ listenerRef.get(),
+ null);
+
+ ScheduledAlarm alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME_WAKEUP);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getTag()).isEqualTo("tag");
+
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+
+ alarm = shadowOf(alarmManager).peekNextScheduledAlarm();
+ assertThat(alarm).isNotNull();
+ assertThat(alarm.getType()).isEqualTo(AlarmManager.ELAPSED_REALTIME);
+ assertThat(alarm.getTriggerAtMs()).isEqualTo(SystemClock.elapsedRealtime() + 10);
+ assertThat(alarm.getTag()).isEqualTo("tag");
}
- private void assertRepeatingScheduledAlarm(
- long now,
- long interval,
- PendingIntent pendingIntent,
- ShadowAlarmManager.ScheduledAlarm scheduledAlarm) {
- assertThat(scheduledAlarm).isNotNull();
- assertThat(scheduledAlarm.operation).isNotNull();
- assertThat(scheduledAlarm.operation).isSameInstanceAs(pendingIntent);
- assertThat(scheduledAlarm.type).isEqualTo(AlarmManager.ELAPSED_REALTIME);
- assertThat(scheduledAlarm.triggerAtTime).isEqualTo(now);
- assertThat(scheduledAlarm.interval).isEqualTo(interval);
+ private class TestBroadcastListener extends BroadcastReceiver implements AutoCloseable {
+
+ private final Runnable alarm;
+ private final String action;
+
+ @Nullable private PendingIntent pendingIntent;
+
+ TestBroadcastListener(Runnable alarm, String action) {
+ this.alarm = alarm;
+ this.action = action;
+ }
+
+ TestBroadcastListener register() {
+ pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(action), 0);
+ context.registerReceiver(this, new IntentFilter(action));
+ return this;
+ }
+
+ PendingIntent getPendingIntent() {
+ return Objects.requireNonNull(pendingIntent);
+ }
+
+ @Override
+ public void close() {
+ context.unregisterReceiver(this);
+ if (pendingIntent != null) {
+ pendingIntent.cancel();
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Objects.equals(action, intent.getAction())) {
+ alarm.run();
+ }
+ }
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java
index 7ecc1f8bf..a429301d3 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAnimationUtilsTest.java
@@ -5,29 +5,24 @@ import static com.google.common.truth.Truth.assertThat;
import android.R;
import android.app.Activity;
import android.view.animation.AnimationUtils;
-import android.view.animation.LayoutAnimationController;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
-import org.robolectric.Shadows;
@RunWith(AndroidJUnit4.class)
public class ShadowAnimationUtilsTest {
@Test
public void loadAnimation_shouldCreateAnimation() {
- assertThat(AnimationUtils.loadAnimation(Robolectric.setupActivity(Activity.class), R.anim.fade_in)).isNotNull();
+ assertThat(
+ AnimationUtils.loadAnimation(Robolectric.setupActivity(Activity.class), R.anim.fade_in))
+ .isNotNull();
}
@Test
public void loadLayoutAnimation_shouldCreateAnimation() {
- assertThat(AnimationUtils.loadLayoutAnimation(Robolectric.setupActivity(Activity.class), 1)).isNotNull();
- }
-
- @Test
- public void getLoadedFromResourceId_forAnimationController_shouldReturnAnimationResourceId() {
- final LayoutAnimationController anim = AnimationUtils.loadLayoutAnimation(Robolectric.setupActivity(Activity.class), R.anim.fade_in);
- assertThat(Shadows.shadowOf(anim).getLoadedFromResourceId()).isEqualTo(R.anim.fade_in);
+ assertThat(AnimationUtils.loadLayoutAnimation(Robolectric.setupActivity(Activity.class), 1))
+ .isNotNull();
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
index f5a29a8fe..b798a74a3 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
@@ -18,6 +18,7 @@ import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.media.AudioAttributes;
+import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
@@ -31,6 +32,7 @@ import com.google.common.collect.ImmutableList;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
@@ -399,6 +401,240 @@ public class ShadowAudioManagerTest {
@Test
@Config(minSdk = M)
+ public void registerAudioDeviceCallback_availableDevices_onAudioDevicesAddedCallback()
+ throws Exception {
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void setInputDevices_withCallbackRegistered_noNotificationCallback() throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void addInputDevice_callbackRegisteredUnregistered_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ audioManager.unregisterAudioDeviceCallback(callback);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void addInputDevice_withCallbackRegisteredAndNoDevice_deviceAddedAndNotifiesCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void
+ addInputDeviceNoCallbackNotification_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void addInputDevice_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void
+ removeInputDevice_withCallbackRegisteredAndDevicePresent_deviceRemovedAndNotifiesCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verify(callback).onAudioDevicesRemoved(new AudioDeviceInfo[] {device});
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void
+ removeInputDeviceNoCallbackNotification_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void removeInputDevice_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void setOutputDevices_withCallbackRegistered_noNotificationCallback() throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void addOutputDevice_withCallbackRegisteredAndNoDevice_deviceAddedAndNotifiesCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void
+ addOutputDeviceNoCallbackNotification_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void addOutputDevice_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void
+ removeOutputDevice_withCallbackRegisteredAndDevicePresent_deviceRemovedAndNotifiesCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verify(callback).onAudioDevicesRemoved(new AudioDeviceInfo[] {device});
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void
+ removeOutputDeviceNoCallbackNotification_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
+
+ shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
+ public void removeOutputDevice_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+ throws Exception {
+ AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+ audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+ verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+ AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+ shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+ verifyNoMoreInteractions(callback);
+ }
+
+ @Test
+ @Config(minSdk = M)
public void getDevices_criteriaInputs_getsAllInputDevices() throws Exception {
AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java
index 2e5be1301..dfe336b6c 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBitmapTest.java
@@ -149,13 +149,13 @@ public class ShadowBitmapTest {
@Test
public void shouldCreateBitmapWithMatrix() {
Bitmap originalBitmap = create("Original bitmap");
- shadowOf(originalBitmap).setWidth(200);
- shadowOf(originalBitmap).setHeight(200);
+ ((ShadowLegacyBitmap) Shadow.extract(originalBitmap)).setWidth(200);
+ ((ShadowLegacyBitmap) Shadow.extract(originalBitmap)).setHeight(200);
Matrix m = new Matrix();
m.postRotate(90);
Bitmap newBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, 100, 50, m, true);
- ShadowBitmap shadowBitmap = shadowOf(newBitmap);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap);
assertThat(shadowBitmap.getDescription())
.isEqualTo(
"Original bitmap at (0,0) with width 100 and height 50"
@@ -246,8 +246,8 @@ public class ShadowBitmapTest {
public void shouldCopyBitmap() {
Bitmap bitmap = Shadow.newInstanceOf(Bitmap.class);
Bitmap bitmapCopy = bitmap.copy(Bitmap.Config.ARGB_8888, true);
- assertThat(shadowOf(bitmapCopy).getConfig()).isEqualTo(Bitmap.Config.ARGB_8888);
- assertThat(shadowOf(bitmapCopy).isMutable()).isTrue();
+ assertThat(bitmapCopy.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888);
+ assertThat(bitmapCopy.isMutable()).isTrue();
}
@Test(expected = NullPointerException.class)
@@ -538,7 +538,7 @@ public class ShadowBitmapTest {
@Test
public void compress_shouldSucceedForNullPixelData() {
Bitmap bitmap = Shadow.newInstanceOf(Bitmap.class);
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.setWidth(100);
shadowBitmap.setHeight(100);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
@@ -548,15 +548,15 @@ public class ShadowBitmapTest {
@Config(sdk = O)
@Test
public void getBytesPerPixel_O() {
- assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.RGBA_F16)).isEqualTo(8);
+ assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.RGBA_F16)).isEqualTo(8);
}
@Test
public void getBytesPerPixel_preO() {
- assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.ARGB_8888)).isEqualTo(4);
- assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.RGB_565)).isEqualTo(2);
- assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.ARGB_4444)).isEqualTo(2);
- assertThat(ShadowBitmap.getBytesPerPixel(Bitmap.Config.ALPHA_8)).isEqualTo(1);
+ assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.ARGB_8888)).isEqualTo(4);
+ assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.RGB_565)).isEqualTo(2);
+ assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.ARGB_4444)).isEqualTo(2);
+ assertThat(ShadowLegacyBitmap.getBytesPerPixel(Bitmap.Config.ALPHA_8)).isEqualTo(1);
}
@Test(expected = RuntimeException.class)
@@ -642,9 +642,7 @@ public class ShadowBitmapTest {
@Test(expected = IllegalStateException.class)
public void reconfigure_withHardwareBitmap_validDimensionsAndConfig_throws() {
Bitmap original = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
- ShadowBitmap shadowBitmap = Shadow.extract(original);
- shadowBitmap.setConfig(Bitmap.Config.HARDWARE);
-
+ original.setConfig(Bitmap.Config.HARDWARE);
original.reconfigure(100, 100, Bitmap.Config.ARGB_8888);
}
@@ -814,15 +812,17 @@ public class ShadowBitmapTest {
private void createScaledBitmap_expectedUpSize(boolean filter) {
Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 32, 32, filter);
- assertThat(Shadows.shadowOf(scaledBitmap).getBufferedImage().getWidth()).isEqualTo(32);
- assertThat(Shadows.shadowOf(scaledBitmap).getBufferedImage().getHeight()).isEqualTo(32);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
+ assertThat(shadowBitmap.getBufferedImage().getWidth()).isEqualTo(32);
+ assertThat(shadowBitmap.getBufferedImage().getHeight()).isEqualTo(32);
}
private void createScaledBitmap_expectedDownSize(boolean filter) {
Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 10, 10, filter);
- assertThat(Shadows.shadowOf(scaledBitmap).getBufferedImage().getWidth()).isEqualTo(10);
- assertThat(Shadows.shadowOf(scaledBitmap).getBufferedImage().getHeight()).isEqualTo(10);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
+ assertThat(shadowBitmap.getBufferedImage().getWidth()).isEqualTo(10);
+ assertThat(shadowBitmap.getBufferedImage().getHeight()).isEqualTo(10);
}
private void createScaledBitmap_drawOnScaled(boolean filter) {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
index 69d7e7b22..10cb14814 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothAdapterTest.java
@@ -495,6 +495,51 @@ public class ShadowBluetoothAdapterTest {
}
@Test
+ public void closeProfileProxy_severalCallersObserving_allNotified() {
+ BluetoothProfile mockProxy = mock(BluetoothProfile.class);
+ BluetoothProfile.ServiceListener mockServiceListener =
+ mock(BluetoothProfile.ServiceListener.class);
+ BluetoothProfile.ServiceListener mockServiceListener2 =
+ mock(BluetoothProfile.ServiceListener.class);
+ shadowOf(bluetoothAdapter).setProfileProxy(MOCK_PROFILE1, mockProxy);
+
+ bluetoothAdapter.getProfileProxy(
+ RuntimeEnvironment.getApplication(), mockServiceListener, MOCK_PROFILE1);
+ bluetoothAdapter.getProfileProxy(
+ RuntimeEnvironment.getApplication(), mockServiceListener2, MOCK_PROFILE1);
+
+ bluetoothAdapter.closeProfileProxy(MOCK_PROFILE1, mockProxy);
+
+ verify(mockServiceListener).onServiceDisconnected(MOCK_PROFILE1);
+ verify(mockServiceListener2).onServiceDisconnected(MOCK_PROFILE1);
+ }
+
+ @Test
+ public void closeProfileProxy_severalCallersObservingAndClosedTwice_allNotifiedOnce() {
+ BluetoothProfile mockProxy = mock(BluetoothProfile.class);
+ BluetoothProfile.ServiceListener mockServiceListener =
+ mock(BluetoothProfile.ServiceListener.class);
+ BluetoothProfile.ServiceListener mockServiceListener2 =
+ mock(BluetoothProfile.ServiceListener.class);
+ shadowOf(bluetoothAdapter).setProfileProxy(MOCK_PROFILE1, mockProxy);
+
+ bluetoothAdapter.getProfileProxy(
+ RuntimeEnvironment.getApplication(), mockServiceListener, MOCK_PROFILE1);
+ bluetoothAdapter.getProfileProxy(
+ RuntimeEnvironment.getApplication(), mockServiceListener2, MOCK_PROFILE1);
+
+ bluetoothAdapter.closeProfileProxy(MOCK_PROFILE1, mockProxy);
+ verify(mockServiceListener).onServiceConnected(MOCK_PROFILE1, mockProxy);
+ verify(mockServiceListener2).onServiceConnected(MOCK_PROFILE1, mockProxy);
+ verify(mockServiceListener).onServiceDisconnected(MOCK_PROFILE1);
+ verify(mockServiceListener2).onServiceDisconnected(MOCK_PROFILE1);
+
+ bluetoothAdapter.closeProfileProxy(MOCK_PROFILE1, mockProxy);
+ verifyNoMoreInteractions(mockServiceListener);
+ verifyNoMoreInteractions(mockServiceListener2);
+ }
+
+ @Test
public void closeProfileProxy_reversesSetProfileProxy() {
BluetoothProfile mockProxy = mock(BluetoothProfile.class);
BluetoothProfile.ServiceListener mockServiceListener =
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
index a76dbc78c..caed17812 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
@@ -1,13 +1,20 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
+import static android.os.Build.VERSION_CODES.O;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.robolectric.Shadows.shadowOf;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.UUID;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@@ -17,30 +24,451 @@ import org.robolectric.annotation.Config;
@Config(minSdk = JELLY_BEAN_MR2)
public class ShadowBluetoothGattTest {
+ private static final byte[] CHARACTERISTIC_VALUE = new byte[] {'a', 'b', 'c'};
+ private static final int INITIAL_VALUE = -99;
private static final String MOCK_MAC_ADDRESS = "00:11:22:33:AA:BB";
+ private static final String ACTION_CONNECTION = "CONNECT/DISCONNECT";
+ private static final String ACTION_DISCOVER = "DISCOVER";
+ private static final String ACTION_READ = "READ";
+ private static final String ACTION_WRITE = "WRITE";
+
+ private int resultStatus = INITIAL_VALUE;
+ private int resultState = INITIAL_VALUE;
+ private String resultAction;
+ private BluetoothGattCharacteristic resultCharacteristic;
+ private BluetoothGatt bluetoothGatt;
+
+ private static final BluetoothGattService service1 =
+ new BluetoothGattService(
+ UUID.fromString("00000000-0000-0000-0000-0000000000A1"),
+ BluetoothGattService.SERVICE_TYPE_PRIMARY);
+ private static final BluetoothGattService service2 =
+ new BluetoothGattService(
+ UUID.fromString("00000000-0000-0000-0000-0000000000A2"),
+ BluetoothGattService.SERVICE_TYPE_SECONDARY);
+
+ private final BluetoothGattCallback callback =
+ new BluetoothGattCallback() {
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ resultStatus = status;
+ resultState = newState;
+ resultAction = ACTION_CONNECTION;
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ resultStatus = status;
+ resultAction = ACTION_DISCOVER;
+ }
+
+ @Override
+ public void onCharacteristicRead(
+ BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ resultStatus = status;
+ resultCharacteristic = characteristic;
+ resultAction = ACTION_READ;
+ }
+
+ @Override
+ public void onCharacteristicWrite(
+ BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ resultStatus = status;
+ resultCharacteristic = characteristic;
+ resultAction = ACTION_WRITE;
+ }
+ };
+
+ private final BluetoothGattCharacteristic characteristicWithReadProperty =
+ new BluetoothGattCharacteristic(
+ UUID.fromString("00000000-0000-0000-0000-0000000000A3"),
+ BluetoothGattCharacteristic.PROPERTY_READ,
+ BluetoothGattCharacteristic.PERMISSION_READ);
+
+ private final BluetoothGattCharacteristic characteristicWithWriteProperties =
+ new BluetoothGattCharacteristic(
+ UUID.fromString("00000000-0000-0000-0000-0000000000A4"),
+ BluetoothGattCharacteristic.PROPERTY_WRITE
+ | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+ @Before
+ public void setUp() throws Exception {
+ BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
+ bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
+ }
@Test
public void canCreateBluetoothGattViaNewInstance() {
- BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
- BluetoothGatt bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
assertThat(bluetoothGatt).isNotNull();
}
@Test
public void canSetAndGetGattCallback() {
- BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
- BluetoothGatt bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
- BluetoothGattCallback callback = new BluetoothGattCallback() {};
-
shadowOf(bluetoothGatt).setGattCallback(callback);
-
assertThat(shadowOf(bluetoothGatt).getGattCallback()).isEqualTo(callback);
}
- @Config(minSdk = JELLY_BEAN_MR2)
- public void connect_returnsTrue() {
- BluetoothDevice bluetoothDevice = ShadowBluetoothDevice.newInstance(MOCK_MAC_ADDRESS);
- BluetoothGatt bluetoothGatt = ShadowBluetoothGatt.newInstance(bluetoothDevice);
+ @Test
+ public void isNotConnected_beforeConnect() {
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void isConnected_returnsFalseWithoutCallback() {
+ assertThat(bluetoothGatt.connect()).isFalse();
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ }
+
+ @Test
+ public void isConnected_afterConnect() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
assertThat(bluetoothGatt.connect()).isTrue();
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ public void isConnected_afterConnectAndDisconnect() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ bluetoothGatt.connect();
+ bluetoothGatt.disconnect();
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+ }
+
+ @Test
+ public void isNotConnected_afterOnlyDisconnect() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ bluetoothGatt.disconnect();
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultState).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ public void isNotConnected_afterConnectAndDisconnectWithoutCallback() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ bluetoothGatt.connect();
+ shadowOf(bluetoothGatt).setGattCallback(null);
+ bluetoothGatt.disconnect();
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultState).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ }
+
+ @Test
+ public void isNotClosedbeforeClose() {
+ assertThat(shadowOf(bluetoothGatt).isClosed()).isFalse();
+ }
+
+ @Test
+ public void isClosedafterClose() {
+ bluetoothGatt.close();
+ assertThat(shadowOf(bluetoothGatt).isClosed()).isTrue();
+ }
+
+ @Test
+ public void isDisconnected_afterClose() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ bluetoothGatt.connect();
+ bluetoothGatt.close();
+ assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void getConnectionPriority_atInitiation() {
+ assertThat(shadowOf(bluetoothGatt).getConnectionPriority())
+ .isEqualTo(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void requestConnectionPriority_inRange() {
+ boolean res =
+ bluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+ assertThat(shadowOf(bluetoothGatt).getConnectionPriority())
+ .isEqualTo(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER);
+ assertThat(res).isTrue();
+ res = bluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+ assertThat(shadowOf(bluetoothGatt).getConnectionPriority())
+ .isEqualTo(BluetoothGatt.CONNECTION_PRIORITY_BALANCED);
+ assertThat(res).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void requestConnectionPriority_notInRange_throwsException() {
+ assertThrows(IllegalArgumentException.class, () -> bluetoothGatt.requestConnectionPriority(-9));
+ assertThrows(IllegalArgumentException.class, () -> bluetoothGatt.requestConnectionPriority(9));
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void discoverServices_noDiscoverableServices_returnsFalse() {
+ assertThat(bluetoothGatt.discoverServices()).isFalse();
+ assertThat(bluetoothGatt.getServices()).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void getServices_afterAddService() {
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ assertThat(bluetoothGatt.discoverServices()).isFalse();
+ assertThat(bluetoothGatt.getServices()).hasSize(1);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void getServices_afterAddMultipleService() {
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ shadowOf(bluetoothGatt).addDiscoverableService(service2);
+ assertThat(bluetoothGatt.discoverServices()).isFalse();
+ assertThat(bluetoothGatt.getServices()).hasSize(2);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void getServices_noDiscoverableServices_withCallback() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ assertThat(bluetoothGatt.discoverServices()).isFalse();
+ assertThat(bluetoothGatt.getServices()).isEmpty();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void getServices_afterAddService_withCallback() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ assertThat(bluetoothGatt.discoverServices()).isTrue();
+ assertThat(bluetoothGatt.getServices()).hasSize(1);
+ assertThat(bluetoothGatt.getServices()).contains(service1);
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_DISCOVER);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void getServices_afterAddMultipleService_withCallback() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ shadowOf(bluetoothGatt).addDiscoverableService(service2);
+ assertThat(bluetoothGatt.discoverServices()).isTrue();
+ assertThat(bluetoothGatt.getServices()).hasSize(2);
+ assertThat(bluetoothGatt.getServices()).contains(service1);
+ assertThat(bluetoothGatt.getServices()).contains(service2);
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_DISCOVER);
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void discoverServices_clearsService() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ shadowOf(bluetoothGatt).addDiscoverableService(service1);
+ shadowOf(bluetoothGatt).removeDiscoverableService(service1);
+ shadowOf(bluetoothGatt).removeDiscoverableService(service2);
+ assertThat(bluetoothGatt.discoverServices()).isFalse();
+ assertThat(bluetoothGatt.getServices()).isEmpty();
+ }
+
+ @Test
+ @Config
+ public void readIncomingCharacteristic_withoutCallback() {
+ assertThrows(
+ IllegalStateException.class,
+ () -> shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithReadProperty));
+ }
+
+ @Test
+ @Config
+ public void readIncomingCharacteristic_withCallback() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ assertThat(shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithReadProperty))
+ .isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ assertThat(resultCharacteristic).isNull();
+ assertThat(shadowOf(bluetoothGatt).getLatestReadBytes()).isNull();
+ }
+
+ @Test
+ @Config
+ public void readIncomingCharacteristic_withCallbackAndServiceSet() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service1.addCharacteristic(characteristicWithReadProperty);
+ assertThat(characteristicWithReadProperty.getService()).isNotNull();
+ assertThat(shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithReadProperty))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_READ);
+ assertThat(resultCharacteristic).isEqualTo(characteristicWithReadProperty);
+ assertThat(shadowOf(bluetoothGatt).getLatestReadBytes()).isNull();
+ }
+
+ @Test
+ @Config
+ public void readIncomingCharacteristic_withCallbackAndServiceSet_withValue() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service1.addCharacteristic(characteristicWithReadProperty);
+ assertThat(characteristicWithReadProperty.getService()).isNotNull();
+ characteristicWithReadProperty.setValue(CHARACTERISTIC_VALUE);
+ assertThat(shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithReadProperty))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_READ);
+ assertThat(resultCharacteristic).isEqualTo(characteristicWithReadProperty);
+ assertThat(shadowOf(bluetoothGatt).getLatestReadBytes()).isEqualTo(CHARACTERISTIC_VALUE);
+ }
+
+ @Test
+ @Config
+ public void readIncomingCharacteristic_withCallbackAndServiceSet_wrongProperty() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service1.addCharacteristic(characteristicWithWriteProperties);
+ assertThat(characteristicWithWriteProperties.getService()).isNotNull();
+ assertThat(
+ shadowOf(bluetoothGatt).readIncomingCharacteristic(characteristicWithWriteProperties))
+ .isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ assertThat(resultCharacteristic).isNull();
+ assertThat(shadowOf(bluetoothGatt).getLatestReadBytes()).isNull();
+ }
+
+ @Test
+ @Config
+ public void writeIncomingCharacteristic_withoutCallback() {
+ service1.addCharacteristic(characteristicWithWriteProperties);
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties));
+ }
+
+ @Test
+ @Config
+ public void writeIncomingCharacteristic_withCallbackOnly() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ assertThat(
+ shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties))
+ .isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ assertThat(resultCharacteristic).isNull();
+ assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+ }
+
+ @Test
+ @Config
+ public void writeIncomingCharacteristic_withCallbackAndServiceSet() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service2.addCharacteristic(characteristicWithWriteProperties);
+ assertThat(characteristicWithWriteProperties.getService()).isNotNull();
+ assertThat(
+ shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_WRITE);
+ assertThat(resultCharacteristic).isEqualTo(characteristicWithWriteProperties);
+ assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+ }
+
+ @Test
+ @Config
+ public void writeIncomingCharacteristic_withCallbackAndServiceSet_wrongProperty() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service1.addCharacteristic(characteristicWithReadProperty);
+ assertThat(characteristicWithReadProperty.getService()).isNotNull();
+ assertThat(shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithReadProperty))
+ .isFalse();
+ assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+ assertThat(resultAction).isNull();
+ assertThat(resultCharacteristic).isNull();
+ assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+ assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+ }
+
+ @Test
+ @Config
+ public void writeIncomingCharacteristic_correctlySetup_noValue() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service1.addCharacteristic(characteristicWithWriteProperties);
+ assertThat(characteristicWithWriteProperties.getService()).isNotNull();
+ assertThat(
+ shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_WRITE);
+ assertThat(resultCharacteristic).isEqualTo(characteristicWithWriteProperties);
+ assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isNull();
+ }
+
+ @Test
+ @Config
+ public void writeIncomingCharacteristic_correctlySetup_withValue() {
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service1.addCharacteristic(characteristicWithWriteProperties);
+ characteristicWithWriteProperties.setValue(CHARACTERISTIC_VALUE);
+ assertThat(characteristicWithWriteProperties.getService()).isNotNull();
+ assertThat(
+ shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristicWithWriteProperties))
+ .isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_WRITE);
+ assertThat(resultCharacteristic).isEqualTo(characteristicWithWriteProperties);
+
+ assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
+ }
+
+ @Test
+ @Config
+ public void writeIncomingCharacteristic_correctlySetup_onlyWriteProperty() {
+
+ BluetoothGattCharacteristic characteristic =
+ new BluetoothGattCharacteristic(
+ UUID.fromString("00000000-0000-0000-0000-0000000000A6"),
+ BluetoothGattCharacteristic.PROPERTY_WRITE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service1.addCharacteristic(characteristic);
+ characteristic.setValue(CHARACTERISTIC_VALUE);
+ assertThat(shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristic)).isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_WRITE);
+ assertThat(resultCharacteristic).isEqualTo(characteristic);
+ assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
+ }
+
+ @Test
+ @Config
+ public void writeIncomingCharacteristic_correctlySetup_onlyWriteNoResponseProperty() {
+
+ BluetoothGattCharacteristic characteristic =
+ new BluetoothGattCharacteristic(
+ UUID.fromString("00000000-0000-0000-0000-0000000000A7"),
+ BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
+ BluetoothGattCharacteristic.PERMISSION_WRITE);
+
+ shadowOf(bluetoothGatt).setGattCallback(callback);
+ service1.addCharacteristic(characteristic);
+ characteristic.setValue(CHARACTERISTIC_VALUE);
+ assertThat(shadowOf(bluetoothGatt).writeIncomingCharacteristic(characteristic)).isTrue();
+ assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+ assertThat(resultAction).isEqualTo(ACTION_WRITE);
+ assertThat(resultCharacteristic).isEqualTo(characteristic);
+ assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
index 9a13954db..9482ba8d7 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
@@ -2,6 +2,7 @@ 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 android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
@@ -174,6 +175,20 @@ public class ShadowBluetoothHeadsetTest {
}
@Test
+ @Config(minSdk = S)
+ public void isVoiceRecognitionSupported_supportedByDefault() {
+ assertThat(bluetoothHeadset.isVoiceRecognitionSupported(device1)).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = S)
+ public void setVoiceRecognitionSupported_false_notSupported() {
+ shadowOf(bluetoothHeadset).setVoiceRecognitionSupported(false);
+
+ assertThat(bluetoothHeadset.isVoiceRecognitionSupported(device1)).isFalse();
+ }
+
+ @Test
@Config(minSdk = KITKAT)
public void sendVendorSpecificResultCode_defaultsToTrueForConnectedDevice() {
shadowOf(bluetoothHeadset).addConnectedDevice(device1);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
index 09a3b5427..7d36f356a 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowDevicePolicyManagerTest.java
@@ -9,6 +9,9 @@ import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_DEF
import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER;
import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE;
import static android.app.admin.DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
import static android.app.admin.DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT;
import static android.app.admin.DevicePolicyManager.STATE_USER_SETUP_COMPLETE;
@@ -1698,6 +1701,91 @@ public final class ShadowDevicePolicyManagerTest {
}
@Test
+ @Config(minSdk = P)
+ public void getLockTaskFeatures_nullAdmin_throwsNullPointerException() {
+ shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+ assertThrows(NullPointerException.class, () -> devicePolicyManager.getLockTaskFeatures(null));
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void getLockTaskFeatures_notOwner_throwsSecurityException() {
+ assertThrows(
+ SecurityException.class, () -> devicePolicyManager.getLockTaskFeatures(testComponent));
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void getLockTaskFeatures_default_noFeatures() {
+ shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+ assertThat(devicePolicyManager.getLockTaskFeatures(testComponent)).isEqualTo(0);
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void setLockTaskFeatures_nullAdmin_throwsNullPointerException() {
+ shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+ assertThrows(
+ NullPointerException.class, () -> devicePolicyManager.setLockTaskFeatures(null, 0));
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void setLockTaskFeatures_notOwner_throwsSecurityException() {
+ assertThrows(
+ SecurityException.class, () -> devicePolicyManager.setLockTaskFeatures(testComponent, 0));
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void setLockTaskFeatures_overviewWithoutHome_throwsIllegalArgumentException() {
+ shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> devicePolicyManager.setLockTaskFeatures(testComponent, LOCK_TASK_FEATURE_OVERVIEW));
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void setLockTaskFeatures_notificationsWithoutHome_throwsIllegalArgumentException() {
+ shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ devicePolicyManager.setLockTaskFeatures(
+ testComponent, LOCK_TASK_FEATURE_NOTIFICATIONS));
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void setLockTaskFeatures_homeOverviewNotifications_success() {
+ shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+
+ int flags =
+ LOCK_TASK_FEATURE_HOME | LOCK_TASK_FEATURE_OVERVIEW | LOCK_TASK_FEATURE_NOTIFICATIONS;
+ devicePolicyManager.setLockTaskFeatures(testComponent, flags);
+
+ assertThat(devicePolicyManager.getLockTaskFeatures(testComponent)).isEqualTo(flags);
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void setLockTaskFeatures_setFeaturesTwice_keepsLatestFeatures() {
+ shadowOf(devicePolicyManager).setProfileOwner(testComponent);
+ devicePolicyManager.setLockTaskFeatures(testComponent, LOCK_TASK_FEATURE_HOME);
+
+ int flags =
+ LOCK_TASK_FEATURE_HOME | LOCK_TASK_FEATURE_OVERVIEW | LOCK_TASK_FEATURE_NOTIFICATIONS;
+ devicePolicyManager.setLockTaskFeatures(testComponent, flags);
+
+ assertThat(devicePolicyManager.getLockTaskFeatures(testComponent)).isEqualTo(flags);
+ }
+
+ @Test
@Config(minSdk = LOLLIPOP)
public void getLockTaskPackages_notOwner() {
try {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
index d31d1858e..f0196218f 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowImsMmTelManagerTest.java
@@ -11,10 +11,12 @@ import android.os.Build.VERSION_CODES;
import android.telephony.ims.ImsException;
import android.telephony.ims.ImsMmTelManager;
import android.telephony.ims.ImsMmTelManager.CapabilityCallback;
-import android.telephony.ims.ImsMmTelManager.RegistrationCallback;
import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.RegistrationManager;
import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.util.ArraySet;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -34,9 +36,152 @@ public class ShadowImsMmTelManagerTest {
}
@Test
- public void registerImsRegistrationCallback_imsRegistering_onRegisteringInvoked()
+ public void registerImsRegistrationManagerCallback_imsRegistering_onRegisteringInvoked()
throws ImsException {
- RegistrationCallback registrationCallback = mock(RegistrationCallback.class);
+ RegistrationManager.RegistrationCallback registrationCallback =
+ mock(RegistrationManager.RegistrationCallback.class);
+
+ shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+ shadowImsMmTelManager.setImsRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+
+ verify(registrationCallback).onRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+
+ shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+ shadowImsMmTelManager.setImsRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+
+ verifyNoMoreInteractions(registrationCallback);
+ }
+
+ @Test
+ @Config(sdk = {VERSION_CODES.S, Config.NEWEST_SDK})
+ public void registerImsRegistrationManagerCallbackImsAttrs_imsRegistering_onRegisteringInvoked()
+ throws ImsException {
+ RegistrationManager.RegistrationCallback registrationCallback =
+ mock(RegistrationManager.RegistrationCallback.class);
+
+ int imsRegistrationTech = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
+ int imsTransportType = RegistrationManager.getAccessType(imsRegistrationTech);
+ int imsAttributeFlags = 0;
+ ArraySet<String> featureTags = new ArraySet<>();
+
+ ImsRegistrationAttributes imsRegistrationAttrs =
+ new ImsRegistrationAttributes(
+ imsRegistrationTech, imsTransportType, imsAttributeFlags, featureTags);
+
+ shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+ shadowImsMmTelManager.setImsRegistering(imsRegistrationAttrs);
+
+ verify(registrationCallback).onRegistering(imsRegistrationAttrs);
+
+ shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+ shadowImsMmTelManager.setImsRegistering(imsRegistrationAttrs);
+
+ verifyNoMoreInteractions(registrationCallback);
+ }
+
+ @Test
+ public void registerImsRegistrationManagerCallback_imsRegistered_onRegisteredInvoked()
+ throws ImsException {
+ RegistrationManager.RegistrationCallback registrationCallback =
+ mock(RegistrationManager.RegistrationCallback.class);
+ shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+ shadowImsMmTelManager.setImsRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+
+ verify(registrationCallback).onRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+
+ shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+ shadowImsMmTelManager.setImsRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+
+ verifyNoMoreInteractions(registrationCallback);
+ }
+
+ @Test
+ @Config(sdk = {VERSION_CODES.S, Config.NEWEST_SDK})
+ public void registerImsRegistrationManagerCallbackImsAttrs_imsRegistered_onRegisteredInvoked()
+ throws ImsException {
+ RegistrationManager.RegistrationCallback registrationCallback =
+ mock(RegistrationManager.RegistrationCallback.class);
+
+ int imsRegistrationTech = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
+ int imsTransportType = RegistrationManager.getAccessType(imsRegistrationTech);
+ int imsAttributeFlags = 0;
+ ArraySet<String> featureTags = new ArraySet<>();
+
+ ImsRegistrationAttributes imsRegistrationAttrs =
+ new ImsRegistrationAttributes(
+ imsRegistrationTech, imsTransportType, imsAttributeFlags, featureTags);
+
+ shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+ shadowImsMmTelManager.setImsRegistered(imsRegistrationAttrs);
+
+ verify(registrationCallback).onRegistered(imsRegistrationAttrs);
+
+ shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+ shadowImsMmTelManager.setImsRegistered(imsRegistrationAttrs);
+
+ verifyNoMoreInteractions(registrationCallback);
+ }
+
+ @Test
+ public void registerImsRegistrationManagerCallback_imsDeregistered_onDeregisteredInvoked()
+ throws ImsException {
+ RegistrationManager.RegistrationCallback registrationCallback =
+ mock(RegistrationManager.RegistrationCallback.class);
+ shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+ ImsReasonInfo imsReasonInfoWithCallbackRegistered = new ImsReasonInfo();
+ shadowImsMmTelManager.setImsUnregistered(imsReasonInfoWithCallbackRegistered);
+
+ verify(registrationCallback).onUnregistered(imsReasonInfoWithCallbackRegistered);
+
+ ImsReasonInfo imsReasonInfoAfterUnregisteringCallback = new ImsReasonInfo();
+ shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+ shadowImsMmTelManager.setImsUnregistered(imsReasonInfoAfterUnregisteringCallback);
+
+ verifyNoMoreInteractions(registrationCallback);
+ }
+
+ @Test
+ public void
+ registerImsRegistrationManagerCallback_imsTechnologyChangeFailed_onTechnologyChangeFailedInvoked()
+ throws ImsException {
+ RegistrationManager.RegistrationCallback registrationCallback =
+ mock(RegistrationManager.RegistrationCallback.class);
+ shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
+ ImsReasonInfo imsReasonInfoWithCallbackRegistered = new ImsReasonInfo();
+ shadowImsMmTelManager.setOnTechnologyChangeFailed(
+ ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, imsReasonInfoWithCallbackRegistered);
+
+ verify(registrationCallback)
+ .onTechnologyChangeFailed(
+ ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, imsReasonInfoWithCallbackRegistered);
+
+ ImsReasonInfo imsReasonInfoAfterUnregisteringCallback = new ImsReasonInfo();
+ shadowImsMmTelManager.unregisterImsRegistrationCallback(registrationCallback);
+ shadowImsMmTelManager.setOnTechnologyChangeFailed(
+ ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, imsReasonInfoAfterUnregisteringCallback);
+
+ verifyNoMoreInteractions(registrationCallback);
+ }
+
+ @Test
+ public void
+ registerImsMmTelManagerRegistrationManagerCallback_imsNotSupported_imsExceptionThrown() {
+ shadowImsMmTelManager.setImsAvailableOnDevice(false);
+ try {
+ shadowImsMmTelManager.registerImsRegistrationCallback(
+ Runnable::run, mock(RegistrationManager.RegistrationCallback.class));
+ assertWithMessage("Expected ImsException was not thrown").fail();
+ } catch (ImsException e) {
+ assertThat(e.getCode()).isEqualTo(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION);
+ assertThat(e).hasMessageThat().contains("IMS not available on device.");
+ }
+ }
+
+ @Test
+ public void registerImsMmTelManagerRegistrationCallback_imsRegistering_onRegisteringInvoked()
+ throws ImsException {
+ ImsMmTelManager.RegistrationCallback registrationCallback =
+ mock(ImsMmTelManager.RegistrationCallback.class);
shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
shadowImsMmTelManager.setImsRegistering(ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
@@ -49,9 +194,10 @@ public class ShadowImsMmTelManagerTest {
}
@Test
- public void registerImsRegistrationCallback_imsRegistered_onRegisteredInvoked()
+ public void registerImsMmTelManagerRegistrationCallback_imsRegistered_onRegisteredInvoked()
throws ImsException {
- RegistrationCallback registrationCallback = mock(RegistrationCallback.class);
+ ImsMmTelManager.RegistrationCallback registrationCallback =
+ mock(ImsMmTelManager.RegistrationCallback.class);
shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
shadowImsMmTelManager.setImsRegistered(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
@@ -64,9 +210,10 @@ public class ShadowImsMmTelManagerTest {
}
@Test
- public void registerImsRegistrationCallback_imsUnregistered_onUnregisteredInvoked()
+ public void registerImsMmTelManagerRegistrationCallback_imsUnregistered_onUnregisteredInvoked()
throws ImsException {
- RegistrationCallback registrationCallback = mock(RegistrationCallback.class);
+ ImsMmTelManager.RegistrationCallback registrationCallback =
+ mock(ImsMmTelManager.RegistrationCallback.class);
shadowImsMmTelManager.registerImsRegistrationCallback(Runnable::run, registrationCallback);
ImsReasonInfo imsReasonInfoWithCallbackRegistered = new ImsReasonInfo();
shadowImsMmTelManager.setImsUnregistered(imsReasonInfoWithCallbackRegistered);
@@ -81,11 +228,11 @@ public class ShadowImsMmTelManagerTest {
}
@Test
- public void registerImsRegistrationCallback_imsNotSupported_imsExceptionThrown() {
+ public void registerImsMmTelManagerRegistrationCallback_imsNotSupported_imsExceptionThrown() {
shadowImsMmTelManager.setImsAvailableOnDevice(false);
try {
shadowImsMmTelManager.registerImsRegistrationCallback(
- Runnable::run, mock(RegistrationCallback.class));
+ Runnable::run, mock(ImsMmTelManager.RegistrationCallback.class));
assertWithMessage("Expected ImsException was not thrown").fail();
} catch (ImsException e) {
assertThat(e.getCode()).isEqualTo(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION);
@@ -98,7 +245,8 @@ public class ShadowImsMmTelManagerTest {
registerMmTelCapabilityCallback_imsRegistered_availabilityChange_onCapabilitiesStatusChangedInvoked()
throws ImsException {
MmTelCapabilities[] mmTelCapabilities = new MmTelCapabilities[1];
- CapabilityCallback capabilityCallback = new CapabilityCallback() {
+ CapabilityCallback capabilityCallback =
+ new CapabilityCallback() {
@Override
public void onCapabilitiesStatusChanged(MmTelCapabilities capabilities) {
super.onCapabilitiesStatusChanged(capabilities);
@@ -129,7 +277,8 @@ public class ShadowImsMmTelManagerTest {
registerMmTelCapabilityCallback_imsNotRegistered_availabilityChange_onCapabilitiesStatusChangedNotInvoked()
throws ImsException {
MmTelCapabilities[] mmTelCapabilities = new MmTelCapabilities[1];
- CapabilityCallback capabilityCallback = new CapabilityCallback() {
+ CapabilityCallback capabilityCallback =
+ new CapabilityCallback() {
@Override
public void onCapabilitiesStatusChanged(MmTelCapabilities capabilities) {
super.onCapabilitiesStatusChanged(capabilities);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java
new file mode 100644
index 000000000..45c428416
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInsetsControllerTest.java
@@ -0,0 +1,72 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.os.Build;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.R)
+public class ShadowInsetsControllerTest {
+ private ActivityController<Activity> activityController;
+ private Activity activity;
+ private WindowInsetsController controller;
+
+ @Before
+ public void setUp() {
+ activityController = Robolectric.buildActivity(Activity.class);
+ activityController.setup();
+
+ activity = activityController.get();
+ controller = activity.getWindow().getInsetsController();
+ }
+
+ @Test
+ public void statusBar_show_hide_trackedByWindowInsets() {
+ // Responds to hide.
+ controller.hide(WindowInsets.Type.statusBars());
+ assertStatusBarVisibility(/* isVisible= */ false);
+
+ // Responds to show.
+ controller.show(WindowInsets.Type.statusBars());
+ assertStatusBarVisibility(/* isVisible= */ true);
+
+ // Does not respond to different type.
+ controller.hide(WindowInsets.Type.navigationBars());
+ assertStatusBarVisibility(/* isVisible= */ true);
+ }
+
+ @Test
+ public void navigationBar_show_hide_trackedByWindowInsets() {
+ // Responds to hide.
+ controller.hide(WindowInsets.Type.navigationBars());
+ assertNavigationBarVisibility(/* isVisible= */ false);
+
+ // Responds to show.
+ controller.show(WindowInsets.Type.navigationBars());
+ assertNavigationBarVisibility(/* isVisible= */ true);
+
+ // Does not respond to different type.
+ controller.hide(WindowInsets.Type.statusBars());
+ assertNavigationBarVisibility(/* isVisible= */ true);
+ }
+
+ private void assertStatusBarVisibility(boolean isVisible) {
+ WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
+ assertThat(insets.isVisible(WindowInsets.Type.statusBars())).isEqualTo(isVisible);
+ }
+
+ private void assertNavigationBarVisibility(boolean isVisible) {
+ WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
+ assertThat(insets.isVisible(WindowInsets.Type.navigationBars())).isEqualTo(isVisible);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLayoutAnimationControllerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLayoutAnimationControllerTest.java
deleted file mode 100644
index 75a1c8d5f..000000000
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLayoutAnimationControllerTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.robolectric.shadows;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.view.animation.LayoutAnimationController;
-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.Shadows;
-
-@RunWith(AndroidJUnit4.class)
-public class ShadowLayoutAnimationControllerTest {
- private ShadowLayoutAnimationController shadow;
-
- @Before
- public void setup() {
- LayoutAnimationController controller =
- new LayoutAnimationController(ApplicationProvider.getApplicationContext(), null);
- shadow = Shadows.shadowOf(controller);
- }
-
- @Test
- public void testResourceId() {
- int id = 1;
- shadow.setLoadedFromResourceId(1);
- assertThat(shadow.getLoadedFromResourceId()).isEqualTo(id);
- }
-
-}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
index 8ee1f6f97..bd99b4c83 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMatrixTest.java
@@ -2,7 +2,6 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static com.google.common.truth.Truth.assertThat;
-import static org.robolectric.Shadows.shadowOf;
import android.graphics.Matrix;
import android.graphics.PointF;
@@ -11,6 +10,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
@RunWith(AndroidJUnit4.class)
public class ShadowMatrixTest {
@@ -23,7 +23,7 @@ public class ShadowMatrixTest {
m.preTranslate(16, 23);
m.preSkew(42, 108);
- assertThat(shadowOf(m).getPreOperations())
+ assertThat(((ShadowMatrix) Shadow.extract(m)).getPreOperations())
.containsExactly("skew 42.0 108.0", "translate 16.0 23.0", "rotate 4.0 8.0 15.0");
}
@@ -34,7 +34,7 @@ public class ShadowMatrixTest {
m.postTranslate(16, 23);
m.postSkew(42, 108);
- assertThat(shadowOf(m).getPostOperations())
+ assertThat(((ShadowMatrix) Shadow.extract(m)).getPostOperations())
.containsExactly("rotate 4.0 8.0 15.0", "translate 16.0 23.0", "skew 42.0 108.0");
}
@@ -49,7 +49,8 @@ public class ShadowMatrixTest {
m.setRotate(42);
m.setRotate(108);
- assertThat(shadowOf(m).getSetOperations()).containsEntry("rotate", "108.0");
+ assertThat(((ShadowMatrix) Shadow.extract(m)).getSetOperations())
+ .containsEntry("rotate", "108.0");
}
@Test
@@ -59,7 +60,7 @@ public class ShadowMatrixTest {
matrix.preScale(2, 2, 2, 2);
matrix.postScale(3, 3, 3, 3);
- final ShadowMatrix shadow = shadowOf(matrix);
+ final ShadowMatrix shadow = Shadow.extract(matrix);
assertThat(shadow.getSetOperations().get("scale")).isEqualTo("1.0 1.0");
assertThat(shadow.getPreOperations().get(0)).isEqualTo("scale 2.0 2.0 2.0 2.0");
assertThat(shadow.getPostOperations().get(0)).isEqualTo("scale 3.0 3.0 3.0 3.0");
@@ -70,7 +71,7 @@ public class ShadowMatrixTest {
final Matrix matrix = new Matrix();
matrix.setScale(1, 2, 3, 4);
- final ShadowMatrix shadow = shadowOf(matrix);
+ final ShadowMatrix shadow = Shadow.extract(matrix);
assertThat(shadow.getSetOperations().get("scale")).isEqualTo("1.0 2.0 3.0 4.0");
}
@@ -83,7 +84,7 @@ public class ShadowMatrixTest {
matrix2.setScale(3, 4);
matrix2.set(matrix1);
- final ShadowMatrix shadow = shadowOf(matrix2);
+ final ShadowMatrix shadow = Shadow.extract(matrix2);
assertThat(shadow.getSetOperations().get("scale")).isEqualTo("1.0 2.0");
}
@@ -96,7 +97,7 @@ public class ShadowMatrixTest {
matrix2.set(matrix1);
matrix2.set(null);
- final ShadowMatrix shadow = shadowOf(matrix2);
+ final ShadowMatrix shadow = Shadow.extract(matrix2);
assertThat(shadow.getSetOperations()).isEmpty();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
index 2bb0dbf91..2299246bd 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowMediaControllerTest.java
@@ -21,6 +21,7 @@ import android.media.session.MediaController;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
+import android.os.Bundle;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
@@ -134,6 +135,16 @@ public final class ShadowMediaControllerTest {
@Test
@Config(minSdk = LOLLIPOP)
+ public void setAndGetExtras() {
+ String extraKey = "test.extra.key";
+ Bundle extras = new Bundle();
+ extras.putBoolean(extraKey, true);
+ shadowMediaController.setExtras(extras);
+ assertEquals(true, mediaController.getExtras().getBoolean(extraKey, false));
+ }
+
+ @Test
+ @Config(minSdk = LOLLIPOP)
public void registerAndGetCallback() {
List<MediaController.Callback> mockCallbacks = new ArrayList<>();
assertEquals(mockCallbacks, shadowMediaController.getCallbacks());
@@ -151,6 +162,23 @@ public final class ShadowMediaControllerTest {
@Test
@Config(minSdk = LOLLIPOP)
+ public void registerWithHandlerAndGetCallback() {
+ List<MediaController.Callback> mockCallbacks = new ArrayList<>();
+ assertEquals(mockCallbacks, shadowMediaController.getCallbacks());
+
+ MediaController.Callback mockCallback1 = mock(MediaController.Callback.class);
+ mockCallbacks.add(mockCallback1);
+ mediaController.registerCallback(mockCallback1, null);
+ assertEquals(mockCallbacks, shadowMediaController.getCallbacks());
+
+ MediaController.Callback mockCallback2 = mock(MediaController.Callback.class);
+ mockCallbacks.add(mockCallback2);
+ mediaController.registerCallback(mockCallback2, null);
+ assertEquals(mockCallbacks, shadowMediaController.getCallbacks());
+ }
+
+ @Test
+ @Config(minSdk = LOLLIPOP)
public void unregisterCallback() {
List<MediaController.Callback> mockCallbacks = new ArrayList<>();
MediaController.Callback mockCallback1 = mock(MediaController.Callback.class);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java
index cb46834bf..727fce40f 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowNetworkCapabilitiesTest.java
@@ -107,4 +107,11 @@ public class ShadowNetworkCapabilitiesTest {
assertThat(wifiInfo.getSSID()).isEqualTo(String.format("\"%s\"", fakeSsid));
assertThat(wifiInfo.getBSSID()).isEqualTo(fakeBssid);
}
+
+ @Test
+ public void setLinkDownstreamBandwidthKbps() {
+ NetworkCapabilities networkCapabilities = ShadowNetworkCapabilities.newInstance();
+ shadowOf(networkCapabilities).setLinkDownstreamBandwidthKbps(100);
+ assertThat(networkCapabilities.getLinkDownstreamBandwidthKbps()).isEqualTo(100);
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
index 870bb1dc1..e3242fadd 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
@@ -46,6 +46,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@@ -76,6 +77,7 @@ import android.content.pm.ModuleInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.OnPermissionsChangedListener;
import android.content.pm.PackageManager.PackageInfoFlags;
@@ -2038,6 +2040,55 @@ public class ShadowPackageManagerTest {
}
@Test
+ @Config(minSdk = TIRAMISU)
+ public void getPackageInfoAfterT_shouldReturnRequestedPermissions() throws Exception {
+ PackageInfo packageInfo =
+ packageManager.getPackageInfo(
+ context.getPackageName(), PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
+ String[] permissions = packageInfo.requestedPermissions;
+ assertThat(permissions).isNotNull();
+ assertThat(permissions).hasLength(4);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPackageInfoAfterT_uninstalledPackage_includeUninstalled() throws Exception {
+ String packageName = context.getPackageName();
+ shadowOf(packageManager).deletePackage(packageName);
+
+ PackageInfo info =
+ packageManager.getPackageInfo(packageName, PackageInfoFlags.of(MATCH_UNINSTALLED_PACKAGES));
+ assertThat(info).isNotNull();
+ assertThat(info.packageName).isEqualTo(packageName);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPackageInfoAfterT_uninstalledPackage_dontIncludeUninstalled() {
+ String packageName = context.getPackageName();
+ shadowOf(packageManager).deletePackage(packageName);
+
+ try {
+ PackageInfo info = packageManager.getPackageInfo(packageName, PackageInfoFlags.of(0));
+ fail("should have thrown NameNotFoundException:" + info.applicationInfo.flags);
+ } catch (NameNotFoundException e) {
+ // expected
+ }
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPackageInfoAfterT_disabledPackage_includeDisabled() throws Exception {
+ packageManager.setApplicationEnabledSetting(
+ context.getPackageName(), COMPONENT_ENABLED_STATE_DISABLED, 0);
+ PackageInfo info =
+ packageManager.getPackageInfo(
+ context.getPackageName(), PackageInfoFlags.of(MATCH_DISABLED_COMPONENTS));
+ assertThat(info).isNotNull();
+ assertThat(info.packageName).isEqualTo(context.getPackageName());
+ }
+
+ @Test
public void getInstalledPackages_uninstalledPackage_includeUninstalled() {
shadowOf(packageManager).deletePackage(context.getPackageName());
@@ -2064,6 +2115,45 @@ public class ShadowPackageManagerTest {
}
@Test
+ @Config(minSdk = TIRAMISU)
+ public void getInstalledPackagesAfterT_uninstalledPackage_includeUninstalled() {
+ shadowOf(packageManager).deletePackage(context.getPackageName());
+
+ assertThat(packageManager.getInstalledPackages(PackageInfoFlags.of(MATCH_UNINSTALLED_PACKAGES)))
+ .isNotEmpty();
+ assertThat(
+ packageManager
+ .getInstalledPackages(PackageInfoFlags.of(MATCH_UNINSTALLED_PACKAGES))
+ .get(0)
+ .packageName)
+ .isEqualTo(context.getPackageName());
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getInstalledPackagesAfterT_uninstalledPackage_dontIncludeUninstalled() {
+ shadowOf(packageManager).deletePackage(context.getPackageName());
+
+ assertThat(packageManager.getInstalledPackages(PackageInfoFlags.of(0))).isEmpty();
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getInstalledPackagesAfterT_disabledPackage_includeDisabled() {
+ packageManager.setApplicationEnabledSetting(
+ context.getPackageName(), COMPONENT_ENABLED_STATE_DISABLED, 0);
+
+ assertThat(packageManager.getInstalledPackages(PackageInfoFlags.of(MATCH_DISABLED_COMPONENTS)))
+ .isNotEmpty();
+ assertThat(
+ packageManager
+ .getInstalledPackages(PackageInfoFlags.of(MATCH_DISABLED_COMPONENTS))
+ .get(0)
+ .packageName)
+ .isEqualTo(context.getPackageName());
+ }
+
+ @Test
public void testGetPreferredActivities() {
final String packageName = "com.example.dummy";
ComponentName name = new ComponentName(packageName, "LauncherActivity");
@@ -2390,6 +2480,24 @@ public class ShadowPackageManagerTest {
}
@Test
+ @Config(minSdk = TIRAMISU)
+ public void getPackageUid_sdkT() throws NameNotFoundException {
+ shadowOf(packageManager).setPackagesForUid(10, new String[] {"a_name"});
+ assertThat(packageManager.getPackageUid("a_name", PackageInfoFlags.of(0))).isEqualTo(10);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPackageUid_sdkT_shouldThrowNameNotFoundExceptionIfNotExist() {
+ try {
+ packageManager.getPackageUid("a_name", PackageInfoFlags.of(0));
+ fail("should have thrown NameNotFoundException");
+ } catch (PackageManager.NameNotFoundException e) {
+ assertThat(e).hasMessageThat().contains("a_name");
+ }
+ }
+
+ @Test
public void getPackagesForUid_shouldReturnSetPackageName() {
shadowOf(packageManager).setPackagesForUid(10, new String[] {"a_name"});
assertThat(packageManager.getPackagesForUid(10)).asList().containsExactly("a_name");
@@ -2637,7 +2745,7 @@ public class ShadowPackageManagerTest {
}
@Test
- public void getInstalledApplications() {
+ public void getInstalledApplications_noFlags_oldSdk() {
List<ApplicationInfo> installedApplications = packageManager.getInstalledApplications(0);
// Default should include the application under test
@@ -2656,6 +2764,33 @@ public class ShadowPackageManagerTest {
}
@Test
+ @Config(minSdk = TIRAMISU)
+ public void getInstalledApplications_null_throwsException() {
+ assertThrows(Exception.class, () -> packageManager.getInstalledApplications(null));
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getInstalledApplications_noFlags_returnsAllInstalledApplications() {
+ List<ApplicationInfo> installedApplications =
+ packageManager.getInstalledApplications(ApplicationInfoFlags.of(0));
+
+ // Default should include the application under test
+ assertThat(installedApplications).hasSize(1);
+ assertThat(installedApplications.get(0).packageName).isEqualTo("org.robolectric");
+
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = "org.other";
+ packageInfo.applicationInfo = new ApplicationInfo();
+ packageInfo.applicationInfo.packageName = "org.other";
+ shadowOf(packageManager).installPackage(packageInfo);
+
+ installedApplications = packageManager.getInstalledApplications(0);
+ assertThat(installedApplications).hasSize(2);
+ assertThat(installedApplications.get(1).packageName).isEqualTo("org.other");
+ }
+
+ @Test
public void getPermissionInfo() throws Exception {
PermissionInfo permission =
context.getPackageManager().getPermissionInfo("org.robolectric.some_permission", 0);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java
index 019cc7547..52721f069 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPhoneWindowTest.java
@@ -5,12 +5,14 @@ import static org.robolectric.Shadows.shadowOf;
import android.app.Activity;
import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
import android.view.Window;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
public class ShadowPhoneWindowTest {
@@ -36,4 +38,26 @@ public class ShadowPhoneWindowTest {
window.setBackgroundDrawable(drawable);
assertThat(shadowOf(window).getBackgroundDrawable()).isSameInstanceAs(drawable);
}
+
+ @Test
+ @Config(minSdk = VERSION_CODES.R)
+ public void getDecorFitsSystemWindows_noCall_returnsDefault() {
+ ShadowWindow candidate = shadowOf(window);
+ assertThat(candidate).isInstanceOf(ShadowPhoneWindow.class);
+
+ assertThat(((ShadowPhoneWindow) candidate).getDecorFitsSystemWindows()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.R)
+ public void getDecorFitsSystemWindows_recordsLastValue() {
+ ShadowWindow candidate = shadowOf(window);
+ assertThat(candidate).isInstanceOf(ShadowPhoneWindow.class);
+
+ window.setDecorFitsSystemWindows(true);
+ assertThat(((ShadowPhoneWindow) candidate).getDecorFitsSystemWindows()).isTrue();
+
+ window.setDecorFitsSystemWindows(false);
+ assertThat(((ShadowPhoneWindow) candidate).getDecorFitsSystemWindows()).isFalse();
+ }
} \ No newline at end of file
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java
index ee02ce296..3f2417136 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSensorManagerTest.java
@@ -11,12 +11,16 @@ import android.hardware.Sensor;
import android.hardware.SensorDirectChannel;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
+import android.hardware.SensorEventListener2;
import android.hardware.SensorManager;
import android.os.Build;
+import android.os.Looper;
import android.os.MemoryFile;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.base.Optional;
+import java.util.ArrayList;
+import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -226,8 +230,56 @@ public class ShadowSensorManagerTest {
assertThat(sensorManager.getSensorList(0)).isNotNull();
}
- private static class TestSensorEventListener implements SensorEventListener {
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.KITKAT)
+ public void flush_shouldCallOnFlushCompleted() {
+ Sensor accelSensor = ShadowSensor.newInstance(TYPE_ACCELEROMETER);
+ Sensor gyroSensor = ShadowSensor.newInstance(TYPE_GYROSCOPE);
+
+ TestSensorEventListener listener1 = new TestSensorEventListener();
+ TestSensorEventListener listener2 = new TestSensorEventListener();
+ TestSensorEventListener listener3 = new TestSensorEventListener();
+
+ sensorManager.registerListener(listener1, accelSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ sensorManager.registerListener(listener2, accelSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ sensorManager.registerListener(listener2, gyroSensor, SensorManager.SENSOR_DELAY_NORMAL);
+
+ // Call flush with the first listener. It should return true (as the flush
+ // succeeded), and should call onFlushCompleted for all listeners registered for accelSensor.
+ assertThat(sensorManager.flush(listener1)).isTrue();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(listener1.getOnFlushCompletedCalls()).containsExactly(accelSensor);
+ assertThat(listener2.getOnFlushCompletedCalls()).containsExactly(accelSensor);
+ assertThat(listener3.getOnFlushCompletedCalls()).isEmpty();
+
+ // Call flush with the second listener. It should again return true, and should call
+ // onFlushCompleted for all listeners registered for accelSensor and gyroSensor.
+ assertThat(sensorManager.flush(listener2)).isTrue();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // From the two calls to flush, onFlushCompleted should have been called twice for accelSensor
+ // and once for gyroSensor.
+ assertThat(listener1.getOnFlushCompletedCalls()).containsExactly(accelSensor, accelSensor);
+ assertThat(listener2.getOnFlushCompletedCalls())
+ .containsExactly(accelSensor, accelSensor, gyroSensor);
+ assertThat(listener3.getOnFlushCompletedCalls()).isEmpty();
+
+ // Call flush with the third listener. This listener is not registered for any sensors, so it
+ // should return false.
+ assertThat(sensorManager.flush(listener3)).isFalse();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // There should not have been any more onFlushCompleted calls.
+ assertThat(listener1.getOnFlushCompletedCalls()).containsExactly(accelSensor, accelSensor);
+ assertThat(listener2.getOnFlushCompletedCalls())
+ .containsExactly(accelSensor, accelSensor, gyroSensor);
+ assertThat(listener3.getOnFlushCompletedCalls()).isEmpty();
+ }
+
+ private static class TestSensorEventListener implements SensorEventListener2 {
private Optional<SensorEvent> latestSensorEvent = Optional.absent();
+ private List<Sensor> onFlushCompletedCalls = new ArrayList<>();
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
@@ -237,6 +289,15 @@ public class ShadowSensorManagerTest {
latestSensorEvent = Optional.of(event);
}
+ @Override
+ public void onFlushCompleted(Sensor sensor) {
+ onFlushCompletedCalls.add(sensor);
+ }
+
+ public List<Sensor> getOnFlushCompletedCalls() {
+ return onFlushCompletedCalls;
+ }
+
public Optional<SensorEvent> getLatestSensorEvent() {
return latestSensorEvent;
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
index 9ea0e9a12..4e7fb16ce 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
@@ -3,6 +3,8 @@ package org.robolectric.shadows;
import static android.content.Context.TELEPHONY_SUBSCRIPTION_SERVICE;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.R;
+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 org.junit.Assert.assertThrows;
@@ -31,6 +33,14 @@ public class ShadowSubscriptionManagerTest {
getApplicationContext().getSystemService(TELEPHONY_SUBSCRIPTION_SERVICE);
}
+ @Config(minSdk = R)
+ @Test
+ public void shouldGiveActiveDataSubscriptionId() {
+ int testId = 42;
+ ShadowSubscriptionManager.setActiveDataSubscriptionId(testId);
+ assertThat(SubscriptionManager.getActiveDataSubscriptionId()).isEqualTo(testId);
+ }
+
@Test
public void shouldGiveDefaultSubscriptionId() {
int testId = 42;
@@ -161,24 +171,24 @@ public class ShadowSubscriptionManagerTest {
@Test
public void isNetworkRoaming_shouldReturnTrueIfSet() {
- shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /*isNetworkRoaming=*/ true);
+ shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /* isNetworkRoaming= */ true);
assertThat(shadowOf(subscriptionManager).isNetworkRoaming(123)).isTrue();
}
/** Multi act-asserts are discouraged but here we are testing the set+unset. */
@Test
public void isNetworkRoaming_shouldReturnFalseIfUnset() {
- shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /*isNetworkRoaming=*/ true);
+ shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /* isNetworkRoaming= */ true);
assertThat(shadowOf(subscriptionManager).isNetworkRoaming(123)).isTrue();
- shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /*isNetworkRoaming=*/ false);
+ shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /* isNetworkRoaming= */ false);
assertThat(shadowOf(subscriptionManager).isNetworkRoaming(123)).isFalse();
}
/** Multi act-asserts are discouraged but here we are testing the set+clear. */
@Test
public void isNetworkRoaming_shouldReturnFalseOnClear() {
- shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /*isNetworkRoaming=*/ true);
+ shadowOf(subscriptionManager).setNetworkRoamingStatus(123, /* isNetworkRoaming= */ true);
assertThat(shadowOf(subscriptionManager).isNetworkRoaming(123)).isTrue();
shadowOf(subscriptionManager).clearNetworkRoamingStatus();
@@ -305,6 +315,22 @@ public class ShadowSubscriptionManagerTest {
.isEqualTo(123);
}
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPhoneNumber_phoneNumberNotSet_returnsEmptyString() {
+ assertThat(subscriptionManager.getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID))
+ .isEqualTo("");
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getPhoneNumber_setPhoneNumber_returnsPhoneNumber() {
+ shadowOf(subscriptionManager)
+ .setPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "123");
+ assertThat(subscriptionManager.getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID))
+ .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 5f6d6b885..cb6359f8e 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
@@ -33,6 +33,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
@@ -45,6 +46,7 @@ import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowTelephonyManager.createTelephonyDisplayInfo;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build.VERSION;
@@ -962,7 +964,7 @@ public class ShadowTelephonyManagerTest {
@Test
@Config(minSdk = S)
public void setCallComposerStatus() {
- ShadowTelephonyManager.setCallComposerStatus(CALL_COMPOSER_STATUS_ON);
+ telephonyManager.setCallComposerStatus(CALL_COMPOSER_STATUS_ON);
assertThat(telephonyManager.getCallComposerStatus()).isEqualTo(CALL_COMPOSER_STATUS_ON);
}
@@ -1030,4 +1032,12 @@ public class ShadowTelephonyManagerTest {
assertThat(shadowOf(telephonyManager).getVisualVoicemailSmsFilterSettings()).isNull();
}
+
+ @Test
+ @Config(minSdk = Q)
+ public void isEmergencyNumber_telephonyServiceUnavailable_throwsIllegalStateException() {
+ ShadowServiceManager.setServiceAvailability(Context.TELEPHONY_SERVICE, false);
+
+ assertThrows(IllegalStateException.class, () -> telephonyManager.isEmergencyNumber("911"));
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java
index 47789de3b..c86c5e95d 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTypefaceTest.java
@@ -162,7 +162,7 @@ public class ShadowTypefaceTest {
// This invokes the Typeface static initializer, which creates some default typefaces.
Typeface.create("roboto", Typeface.BOLD);
// Call the resetter to clear the FONTS map in Typeface
- ShadowTypeface.reset();
+ ShadowLegacyTypeface.reset();
Typeface typeface =
new Typeface.CustomFallbackBuilder(family).setStyle(font.getStyle()).build();
assertThat(typeface).isNotNull();
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
index d165ec10d..75df1101a 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
@@ -7,7 +7,6 @@ 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.N_MR1;
-import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static com.google.common.truth.Truth.assertThat;
@@ -907,13 +906,13 @@ public class ShadowUserManagerTest {
}
@Test
- @Config(minSdk = O)
+ @Config(minSdk = N)
public void isQuietModeEnabled_shouldReturnFalse() {
assertThat(userManager.isQuietModeEnabled(Process.myUserHandle())).isFalse();
}
@Test
- @Config(minSdk = Q)
+ @Config(minSdk = N)
public void isQuietModeEnabled_withProfile_shouldReturnFalse() {
shadowOf(userManager).addProfile(0, 10, "Work profile", UserInfo.FLAG_MANAGED_PROFILE);
@@ -921,6 +920,16 @@ public class ShadowUserManagerTest {
}
@Test
+ @Config(minSdk = N)
+ public void isQuietModeEnabled_withProfileQuietMode_shouldReturnTrue() {
+ shadowOf(userManager)
+ .addProfile(
+ 0, 10, "Work profile", UserInfo.FLAG_MANAGED_PROFILE | UserInfo.FLAG_QUIET_MODE);
+
+ assertThat(userManager.isQuietModeEnabled(new UserHandle(10))).isTrue();
+ }
+
+ @Test
@Config(minSdk = Q)
public void requestQuietModeEnabled_withoutPermission_shouldThrowException() {
shadowOf(userManager).enforcePermissionChecks(true);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java
new file mode 100644
index 000000000..e10cbb3fa
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowViewRootImplTest.java
@@ -0,0 +1,55 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.os.Build;
+import android.view.View;
+import android.view.WindowInsets;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public class ShadowViewRootImplTest {
+ private ActivityController<Activity> activityController;
+ private Activity activity;
+ private View rootView;
+
+ @Before
+ public void setUp() {
+ activityController = Robolectric.buildActivity(Activity.class);
+ activityController.setup();
+
+ activity = activityController.get();
+ rootView = activity.getWindow().getDecorView();
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.R)
+ public void setIsStatusBarVisible_impactsGetWindowInsets() {
+ ShadowViewRootImpl.setIsStatusBarVisible(false);
+ WindowInsets windowInsets = rootView.getRootWindowInsets();
+ assertThat(windowInsets.isVisible(WindowInsets.Type.statusBars())).isFalse();
+
+ ShadowViewRootImpl.setIsStatusBarVisible(true);
+ windowInsets = rootView.getRootWindowInsets();
+ assertThat(windowInsets.isVisible(WindowInsets.Type.statusBars())).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.R)
+ public void setIsNavigationBarVisible_impactsGetWindowInsets() {
+ ShadowViewRootImpl.setIsNavigationBarVisible(false);
+ WindowInsets windowInsets = rootView.getRootWindowInsets();
+ assertThat(windowInsets.isVisible(WindowInsets.Type.navigationBars())).isFalse();
+
+ ShadowViewRootImpl.setIsNavigationBarVisible(true);
+ windowInsets = rootView.getRootWindowInsets();
+ assertThat(windowInsets.isVisible(WindowInsets.Type.navigationBars())).isTrue();
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java
index 89535f6d0..51e69abfb 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWallpaperManagerTest.java
@@ -3,7 +3,6 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
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.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.fail;
@@ -17,12 +16,11 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
-import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
@@ -43,7 +41,7 @@ public class ShadowWallpaperManagerTest {
private static final Bitmap TEST_IMAGE_3 = Bitmap.createBitmap(1, 5, Bitmap.Config.ARGB_8888);
- private static final int UNSUPPORTED_FLAG = WallpaperManager.FLAG_LOCK + 123;
+ private static final int UNSUPPORTED_FLAG = 0x100; // neither FLAG_SYSTEM nor FLAG_LOCK
private static final String SET_WALLPAPER_COMPONENT =
"android.permission.SET_WALLPAPER_COMPONENT";
@@ -150,7 +148,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void setBitmap_flagSystem_shouldCacheInMemory() throws Exception {
int returnCode =
manager.setBitmap(
@@ -165,7 +163,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void setBitmap_liveWallpaperWasDefault_flagSystem_shouldRemoveLiveWallpaper()
throws Exception {
manager.setWallpaperComponent(TEST_WALLPAPER_SERVICE);
@@ -180,7 +178,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void setBitmap_multipleCallsWithFlagSystem_shouldCacheLastBitmapInMemory()
throws Exception {
manager.setBitmap(
@@ -204,7 +202,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void setBitmap_flagLock_shouldCacheInMemory() throws Exception {
int returnCode =
manager.setBitmap(
@@ -219,7 +217,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void setBitmap_liveWallpaperWasDefault_flagLock_shouldRemoveLiveWallpaper()
throws Exception {
manager.setWallpaperComponent(TEST_WALLPAPER_SERVICE);
@@ -234,7 +232,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void setBitmap_multipleCallsWithFlagLock_shouldCacheLastBitmapInMemory() throws Exception {
manager.setBitmap(
TEST_IMAGE_1,
@@ -257,7 +255,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void setBitmap_unsupportedFlag_shouldNotCacheInMemory() throws Exception {
int code =
manager.setBitmap(
@@ -268,7 +266,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void setBitmap_liveWallpaperWasDefault_unsupportedFlag_shouldNotRemoveLiveWallpaper()
throws Exception {
manager.setWallpaperComponent(TEST_WALLPAPER_SERVICE);
@@ -280,13 +278,13 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void getWallpaperFile_flagSystem_nothingCached_shouldReturnNull() throws Exception {
assertThat(manager.getWallpaperFile(WallpaperManager.FLAG_SYSTEM)).isNull();
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void getWallpaperFile_flagSystem_previouslyCached_shouldReturnParcelFileDescriptor()
throws Exception {
manager.setBitmap(
@@ -303,13 +301,13 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void getWallpaperFile_flagLock_nothingCached_shouldReturnNull() throws Exception {
assertThat(manager.getWallpaperFile(WallpaperManager.FLAG_LOCK)).isNull();
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void getWallpaperFile_flagLock_previouslyCached_shouldReturnParcelFileDescriptor()
throws Exception {
manager.setBitmap(
@@ -326,7 +324,7 @@ public class ShadowWallpaperManagerTest {
}
@Test
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void getWallpaperFile_unsupportedFlag_shouldReturnNull() throws Exception {
assertThat(manager.getWallpaperFile(UNSUPPORTED_FLAG)).isNull();
}
@@ -366,61 +364,47 @@ public class ShadowWallpaperManagerTest {
@Test
@Config(minSdk = N)
public void setStream_flagSystem_shouldCacheInMemory() throws Exception {
- InputStream inputStream = null;
byte[] testImageBytes = getBytesFromBitmap(TEST_IMAGE_1);
- try {
- inputStream = new ByteArrayInputStream(testImageBytes);
- manager.setStream(
- inputStream,
- /* visibleCropHint= */ null,
- /* allowBackup= */ true,
- WallpaperManager.FLAG_SYSTEM);
+
+ manager.setStream(
+ new ByteArrayInputStream(testImageBytes),
+ /* visibleCropHint= */ null,
+ /* allowBackup= */ true,
+ WallpaperManager.FLAG_SYSTEM);
assertThat(getBytesFromBitmap(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)))
.isEqualTo(testImageBytes);
assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)).isNull();
- } finally {
- close(inputStream);
- }
}
@Test
@Config(minSdk = N)
public void setStream_flagLock_shouldCacheInMemory() throws Exception {
- InputStream inputStream = null;
byte[] testImageBytes = getBytesFromBitmap(TEST_IMAGE_2);
- try {
- inputStream = new ByteArrayInputStream(testImageBytes);
- manager.setStream(
- inputStream,
- /* visibleCropHint= */ null,
- /* allowBackup= */ true,
- WallpaperManager.FLAG_LOCK);
+ manager.setStream(
+ new ByteArrayInputStream(testImageBytes),
+ /* visibleCropHint= */ null,
+ /* allowBackup= */ true,
+ WallpaperManager.FLAG_LOCK);
assertThat(getBytesFromBitmap(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)))
.isEqualTo(testImageBytes);
assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)).isNull();
- } finally {
- close(inputStream);
- }
}
@Test
@Config(minSdk = N)
public void setStream_unsupportedFlag_shouldNotCacheInMemory() throws Exception {
- InputStream inputStream = null;
byte[] testImageBytes = getBytesFromBitmap(TEST_IMAGE_2);
- try {
- inputStream = new ByteArrayInputStream(testImageBytes);
- manager.setStream(
- inputStream, /* visibleCropHint= */ null, /* allowBackup= */ true, UNSUPPORTED_FLAG);
+ manager.setStream(
+ new ByteArrayInputStream(testImageBytes),
+ /* visibleCropHint= */ null,
+ /* allowBackup= */ true,
+ UNSUPPORTED_FLAG);
assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)).isNull();
assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)).isNull();
assertThat(shadowOf(manager).getBitmap(UNSUPPORTED_FLAG)).isNull();
- } finally {
- close(inputStream);
- }
}
@Test
@@ -465,7 +449,7 @@ public class ShadowWallpaperManagerTest {
assertThat(manager.getWallpaperInfo()).isNull();
}
- @Config(minSdk = P)
+ @Config(minSdk = N)
public void
getWallpaperInfo_staticWallpaperWasDefault_liveWallpaperSet_shouldRemoveCachedStaticWallpaper()
throws Exception {
@@ -541,39 +525,48 @@ public class ShadowWallpaperManagerTest {
.isEqualTo(1f);
}
+ @Test
+ @Config(minSdk = N)
+ public void setBitmap_bothLockAndHome() throws Exception {
+ int returnCode =
+ manager.setBitmap(
+ TEST_IMAGE_1,
+ /* visibleCropHint= */ null,
+ /* allowBackup= */ false,
+ WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK);
+
+ assertThat(returnCode).isEqualTo(1);
+ assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)).isEqualTo(TEST_IMAGE_1);
+ assertThat(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)).isEqualTo(TEST_IMAGE_1);
+ }
+
+ @Test
+ @Config(minSdk = N)
+ public void setStream_bothLockAndHome() throws Exception {
+ byte[] testImageBytes = getBytesFromBitmap(TEST_IMAGE_1);
+ manager.setStream(
+ new ByteArrayInputStream(testImageBytes),
+ /* visibleCropHint= */ null,
+ /* allowBackup= */ true,
+ WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK);
+
+ assertThat(getBytesFromBitmap(shadowOf(manager).getBitmap(WallpaperManager.FLAG_SYSTEM)))
+ .isEqualTo(testImageBytes);
+ assertThat(getBytesFromBitmap(shadowOf(manager).getBitmap(WallpaperManager.FLAG_LOCK)))
+ .isEqualTo(testImageBytes);
+ }
+
private static byte[] getBytesFromFileDescriptor(FileDescriptor fileDescriptor)
throws IOException {
- FileInputStream inputStream = null;
- ByteArrayOutputStream outputStream = null;
- try {
- inputStream = new FileInputStream(fileDescriptor);
- outputStream = new ByteArrayOutputStream();
- byte[] buffer = new byte[1024];
- int numOfBytes = 0;
- while ((numOfBytes = inputStream.read(buffer, 0, buffer.length)) != -1) {
- outputStream.write(buffer, 0, numOfBytes);
- }
+ InputStream inputStream = new FileInputStream(fileDescriptor);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ByteStreams.copy(inputStream, outputStream);
return outputStream.toByteArray();
- } finally {
- close(inputStream);
- close(outputStream);
- }
- }
-
- private static byte[] getBytesFromBitmap(Bitmap bitmap) throws IOException {
- ByteArrayOutputStream stream = null;
- try {
- stream = new ByteArrayOutputStream();
- bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 0, stream);
- return stream.toByteArray();
- } finally {
- close(stream);
- }
}
- private static void close(@Nullable Closeable closeable) throws IOException {
- if (closeable != null) {
- closeable.close();
- }
+ private static byte[] getBytesFromBitmap(Bitmap bitmap) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 0, stream);
+ return stream.toByteArray();
}
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
index 593327ab3..299a85333 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
@@ -675,6 +675,21 @@ public class ShadowWifiManagerTest {
}
@Test
+ @Config(minSdk = R)
+ public void testSetClearWifiConnectedNetworkScorer() {
+ // GIVEN
+ WifiManager.WifiConnectedNetworkScorer mockScorer =
+ mock(WifiManager.WifiConnectedNetworkScorer.class);
+ // WHEN
+ wifiManager.setWifiConnectedNetworkScorer(directExecutor(), mockScorer);
+ assertThat(shadowOf(wifiManager).isWifiConnectedNetworkScorerEnabled()).isTrue();
+ wifiManager.clearWifiConnectedNetworkScorer();
+
+ // THEN
+ assertThat(shadowOf(wifiManager).isWifiConnectedNetworkScorerEnabled()).isFalse();
+ }
+
+ @Test
@Config(minSdk = Q)
public void testGetUsabilityScores() {
// GIVEN
diff --git a/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java b/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
index 3a549a763..ce81f2a61 100644
--- a/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
+++ b/sandbox/src/main/java/org/robolectric/config/AndroidConfigurer.java
@@ -60,6 +60,7 @@ public class AndroidConfigurer {
.doNotAcquirePackage("jdk.internal.")
.doNotAcquirePackage("org.junit")
.doNotAcquirePackage("org.hamcrest")
+ .doNotAcquirePackage("org.objectweb.asm")
.doNotAcquirePackage("org.robolectric.annotation.")
.doNotAcquirePackage("org.robolectric.internal.")
.doNotAcquirePackage("org.robolectric.pluginapi.")
@@ -98,8 +99,7 @@ public class AndroidConfigurer {
}
// Instrumenting these classes causes a weird failure.
- builder.doNotInstrumentClass("android.R")
- .doNotInstrumentClass("android.R$styleable");
+ builder.doNotInstrumentClass("android.R").doNotInstrumentClass("android.R$styleable");
builder
.addInstrumentedPackage("dalvik.")
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 53872c13b..00e200941 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
@@ -49,7 +49,7 @@ public class ClassInstrumentor {
private static final Handle BOOTSTRAP_STATIC;
private static final Handle BOOTSTRAP_INTRINSIC;
private static final String ROBO_INIT_METHOD_NAME = "$$robo$init";
- static final Type OBJECT_TYPE = Type.getType(Object.class);
+ protected static final Type OBJECT_TYPE = Type.getType(Object.class);
private static final ShadowImpl SHADOW_IMPL = new ShadowImpl();
final Decorator decorator;
@@ -175,8 +175,6 @@ public class ClassInstrumentor {
// If there is no constructor, adds one
addNoArgsConstructor(mutableClass);
- addDirectCallConstructor(mutableClass);
-
addRoboInitMethod(mutableClass);
removeFinalFromFields(mutableClass);
@@ -236,20 +234,27 @@ public class ClassInstrumentor {
* Adds a call $$robo$init, which instantiates a shadow object if required. This is to support
* custom shadows for Jacoco-instrumented classes (except cnstructor shadows).
*/
- private void addCallToRoboInit(MutableClass mutableClass, MethodNode ctor) {
+ protected void addCallToRoboInit(MutableClass mutableClass, MethodNode ctor) {
AbstractInsnNode returnNode =
Iterables.find(
ctor.instructions,
- node -> node instanceof InsnNode && node.getOpcode() == Opcodes.RETURN,
+ node -> {
+ if (node.getOpcode() == Opcodes.INVOKESPECIAL) {
+ MethodInsnNode mNode = (MethodInsnNode) node;
+ return (mNode.owner.equals(mutableClass.internalClassName)
+ || mNode.owner.equals(mutableClass.classNode.superName));
+ }
+ return false;
+ },
null);
- ctor.instructions.insertBefore(returnNode, new VarInsnNode(Opcodes.ALOAD, 0));
- ctor.instructions.insertBefore(
+ ctor.instructions.insert(
returnNode,
new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
mutableClass.classType.getInternalName(),
ROBO_INIT_METHOD_NAME,
"()V"));
+ ctor.instructions.insert(returnNode, new VarInsnNode(Opcodes.ALOAD, 0));
}
private void instrumentMethods(MutableClass mutableClass) {
@@ -292,8 +297,6 @@ public class ClassInstrumentor {
}
}
- protected void addDirectCallConstructor(MutableClass mutableClass) {}
-
/**
* Generates code like this:
*
@@ -351,12 +354,24 @@ public class ClassInstrumentor {
}
/**
- * Constructors are instrumented as follows: TODO(slliu): Fill in constructor instrumentation
- * directions
+ * Constructors are instrumented as follows:
+ *
+ * <ul>
+ * <li>The original constructor will be stripped of its instructions leading up to, and
+ * including, the call to super() or this(). It is also renamed to $$robo$$__constructor__
+ * <li>A method called __constructor__ is created and its job is to call
+ * $$robo$$__constructor__. The __constructor__ method is what gets shadowed if a Shadow
+ * wants to shadow a constructor.
+ * <li>A new constructor is created and contains the stripped instructions of the original
+ * constructor leading up to, and including, the call to super() or this(). Then, it has a
+ * call to $$robo$init to initialize the Class' Shadow Object. Then, it uses invokedynamic
+ * to call __constructor__. Finally, it contains any instructions that might occur after the
+ * return statement in the original constructor.
+ * </ul>
*
* @param method the constructor to instrument
*/
- private void instrumentConstructor(MutableClass mutableClass, MethodNode method) {
+ protected void instrumentConstructor(MutableClass mutableClass, MethodNode method) {
makeMethodPrivate(method);
InsnList callSuper = extractCallToSuperConstructor(mutableClass, method);
@@ -488,7 +503,8 @@ public class ClassInstrumentor {
instrumentNativeMethod(mutableClass, method);
}
- // todo figure out
+ // Create delegator method with same name as original method. The delegator method will use
+ // invokedynamic to decide at runtime whether to call original method or shadowed method
String originalName = method.name;
method.name = directMethodName(mutableClass, originalName);
@@ -505,7 +521,6 @@ public class ClassInstrumentor {
generator.endMethod();
mutableClass.addMethod(delegatorMethodNode);
}
-
/**
* Creates native stub which returns the default return value.
*
@@ -715,6 +730,14 @@ public class ClassInstrumentor {
return Modifier.isStatic(m.access) ? Opcodes.H_INVOKESTATIC : Opcodes.H_INVOKESPECIAL;
}
+ // implemented in DirectClassInstrumentor
+ public void setAndroidJarSDKVersion(int androidJarSDKVersion) {}
+
+ // implemented in DirectClassInstrumentor
+ protected int getAndroidJarSDKVersion() {
+ return -1;
+ }
+
public interface Decorator {
void decorate(MutableClass mutableClass);
}
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java
index 305431f7a..63b4b2002 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/MutableClass.java
@@ -54,6 +54,10 @@ public class MutableClass {
return new ArrayList<>(classNode.methods);
}
+ public Type getClassType() {
+ return classType;
+ }
+
public void addMethod(MethodNode methodNode) {
classNode.methods.add(methodNode);
}
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java
index 401cae184..cb77a1f74 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowMap.java
@@ -19,11 +19,11 @@ import org.robolectric.util.Logger;
/**
* Maps from instrumented class to shadow class.
*
- * We deal with class names rather than actual classes here, since a ShadowMap is built outside of
- * any sandboxes, but instrumented and shadowed classes must be loaded through a
- * {@link SandboxClassLoader}. We don't want to try to resolve those classes outside of a sandbox.
+ * <p>We deal with class names rather than actual classes here, since a ShadowMap is built outside
+ * of any sandboxes, but instrumented and shadowed classes must be loaded through a {@link
+ * SandboxClassLoader}. We don't want to try to resolve those classes outside of a sandbox.
*
- * Once constructed, instances are immutable.
+ * <p>Once constructed, instances are immutable.
*/
@SuppressWarnings("NewApi")
public class ShadowMap {
@@ -69,6 +69,10 @@ public class ShadowMap {
this.shadowPickers = ImmutableMap.copyOf(shadowPickers);
}
+ public boolean hasShadowPicker(MutableClass mutableClass) {
+ return shadowPickers.containsKey(mutableClass.getName().replace('$', '.'));
+ }
+
public ShadowInfo getShadowInfo(Class<?> clazz, ShadowMatcher shadowMatcher) {
String instrumentedClassName = clazz.getName();
@@ -117,8 +121,8 @@ public class ShadowMap {
return pickShadow(instrumentedClassName, clazz, shadowPickerClassName);
}
- private ShadowInfo pickShadow(String instrumentedClassName, Class<?> clazz,
- String shadowPickerClassName) {
+ private ShadowInfo pickShadow(
+ String instrumentedClassName, Class<?> clazz, String shadowPickerClassName) {
ClassLoader sandboxClassLoader = clazz.getClassLoader();
try {
Class<? extends ShadowPicker<?>> shadowPickerClass =
@@ -131,16 +135,22 @@ public class ShadowMap {
ShadowInfo shadowInfo = obtainShadowInfo(selectedShadowClass);
if (!shadowInfo.shadowedClassName.equals(instrumentedClassName)) {
- throw new IllegalArgumentException("Implemented class for "
- + selectedShadowClass.getName() + " (" + shadowInfo.shadowedClassName + ") != "
- + instrumentedClassName);
+ throw new IllegalArgumentException(
+ "Implemented class for "
+ + selectedShadowClass.getName()
+ + " ("
+ + shadowInfo.shadowedClassName
+ + ") != "
+ + instrumentedClassName);
}
return shadowInfo;
- } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
- | IllegalAccessException | InstantiationException e) {
- throw new RuntimeException("Failed to resolve shadow picker for " + instrumentedClassName,
- e);
+ } catch (ClassNotFoundException
+ | NoSuchMethodException
+ | InvocationTargetException
+ | IllegalAccessException
+ | InstantiationException e) {
+ throw new RuntimeException("Failed to resolve shadow picker for " + instrumentedClassName, e);
}
}
@@ -227,7 +237,7 @@ public class ShadowMap {
private final Map<String, ShadowInfo> overriddenShadows;
private final Map<String, String> shadowPickers;
- public Builder () {
+ public Builder() {
defaultShadows = ImmutableListMultimap.of();
overriddenShadows = new HashMap<>();
shadowPickers = new HashMap<>();
@@ -265,8 +275,8 @@ public class ShadowMap {
private void addShadowInfo(ShadowInfo shadowInfo) {
overriddenShadows.put(shadowInfo.shadowedClassName, shadowInfo);
if (shadowInfo.hasShadowPicker()) {
- shadowPickers
- .put(shadowInfo.shadowedClassName, shadowInfo.getShadowPickerClass().getName());
+ shadowPickers.put(
+ shadowInfo.shadowedClassName, shadowInfo.getShadowPickerClass().getName());
}
}
diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle
index 21160b61a..cd95bb106 100644
--- a/shadows/framework/build.gradle
+++ b/shadows/framework/build.gradle
@@ -55,7 +55,7 @@ dependencies {
compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
api "com.ibm.icu:icu4j:70.1"
api "androidx.annotation:annotation:1.1.0"
- api "com.google.auto.value:auto-value-annotations:1.9"
+ api "com.google.auto.value:auto-value-annotations:1.10"
annotationProcessor "com.google.auto.value:auto-value:1.9"
sqlite4java "com.almworks.sqlite4java:libsqlite4java-osx:$sqlite4javaVersion"
diff --git a/shadows/framework/src/main/java/android/media/Session2Token.java b/shadows/framework/src/main/java/android/media/Session2Token.java
deleted file mode 100644
index 4a321e7b2..000000000
--- a/shadows/framework/src/main/java/android/media/Session2Token.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package android.media;
-
-/**
- * Temporary replacement for class missing in Android Q Preview 1.
- *
- * TODO: Remove for Q Preview 2.
- */
-public class Session2Token {
-
-}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java b/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
index 75b495744..c9a723ca2 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ImageUtil.java
@@ -26,7 +26,6 @@ import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
-import org.robolectric.Shadows;
import org.robolectric.shadow.api.Shadow;
public class ImageUtil {
@@ -117,7 +116,7 @@ public class ImageUtil {
if (srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) {
return false;
}
- BufferedImage before = ((ShadowBitmap) Shadow.extract(src)).getBufferedImage();
+ BufferedImage before = ((ShadowLegacyBitmap) Shadow.extract(src)).getBufferedImage();
if (before == null || before.getColorModel() == null) {
return false;
}
@@ -129,7 +128,7 @@ public class ImageUtil {
filter ? VALUE_INTERPOLATION_BILINEAR : VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
graphics2D.drawImage(before, 0, 0, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, null);
graphics2D.dispose();
- ((ShadowBitmap) Shadow.extract(dst)).setBufferedImage(after);
+ ((ShadowLegacyBitmap) Shadow.extract(dst)).setBufferedImage(after);
return true;
}
@@ -156,7 +155,8 @@ public class ImageUtil {
int width = realBitmap.getWidth();
int height = realBitmap.getHeight();
boolean needAlphaChannel = needAlphaChannel(format);
- BufferedImage bufferedImage = Shadows.shadowOf(realBitmap).getBufferedImage();
+ BufferedImage bufferedImage =
+ ((ShadowLegacyBitmap) Shadow.extract(realBitmap)).getBufferedImage();
if (bufferedImage == null) {
bufferedImage =
new BufferedImage(
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ResponderLocationBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ResponderLocationBuilder.java
new file mode 100644
index 000000000..794e75d31
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ResponderLocationBuilder.java
@@ -0,0 +1,211 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.net.wifi.rtt.ResponderLocation;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link ResponderLocation} */
+@SuppressWarnings("CanIgnoreReturnValueSuggester")
+public class ResponderLocationBuilder {
+ // LCI Subelement LCI state
+ private Double altitude;
+ private Double altitudeUncertainty;
+ private Integer altitudeType;
+ private Double latitudeDegrees;
+ private Double latitudeUncertainty;
+ private Double longitudeDegrees;
+ private Double longitudeUncertainty;
+ private Integer datum;
+ private Integer lciVersion;
+ private Boolean lciRegisteredLocationAgreement;
+
+ // LCI Subelement Z state
+ private Double heightAboveFloorMeters;
+ private Double heightAboveFloorUncertaintyMeters;
+ private Integer expectedToMove;
+ private Double floorNumber;
+
+ private ResponderLocationBuilder() {}
+
+ public static ResponderLocationBuilder newBuilder() {
+ return new ResponderLocationBuilder();
+ }
+
+ public ResponderLocationBuilder setAltitude(double altitude) {
+ this.altitude = altitude;
+ return this;
+ }
+
+ public ResponderLocationBuilder setAltitudeUncertainty(double altitudeUncertainty) {
+ this.altitudeUncertainty = altitudeUncertainty;
+ return this;
+ }
+
+ public ResponderLocationBuilder setAltitudeType(int altitudeType) {
+ this.altitudeType = altitudeType;
+ return this;
+ }
+
+ public ResponderLocationBuilder setLatitude(double latitudeDegrees) {
+ this.latitudeDegrees = latitudeDegrees;
+ return this;
+ }
+
+ public ResponderLocationBuilder setLatitudeUncertainty(double latitudeUncertainty) {
+ this.latitudeUncertainty = latitudeUncertainty;
+ return this;
+ }
+
+ public ResponderLocationBuilder setLongitude(double longitudeDegrees) {
+ this.longitudeDegrees = longitudeDegrees;
+ return this;
+ }
+
+ public ResponderLocationBuilder setLongitudeUncertainty(double longitudeUncertainty) {
+ this.longitudeUncertainty = longitudeUncertainty;
+ return this;
+ }
+
+ public ResponderLocationBuilder setDatum(int datum) {
+ this.datum = datum;
+ return this;
+ }
+
+ public ResponderLocationBuilder setLciVersion(int lciVersion) {
+ this.lciVersion = lciVersion;
+ return this;
+ }
+
+ public ResponderLocationBuilder setLciRegisteredLocationAgreement(
+ Boolean lciRegisteredLocationAgreement) {
+ this.lciRegisteredLocationAgreement = lciRegisteredLocationAgreement;
+ return this;
+ }
+
+ public ResponderLocationBuilder setHeightAboveFloorMeters(double heightAboveFloorMeters) {
+ this.heightAboveFloorMeters = heightAboveFloorMeters;
+ return this;
+ }
+
+ public ResponderLocationBuilder setHeightAboveFloorUncertaintyMeters(
+ double heightAboveFloorUncertaintyMeters) {
+ this.heightAboveFloorUncertaintyMeters = heightAboveFloorUncertaintyMeters;
+ return this;
+ }
+
+ public ResponderLocationBuilder setExpectedToMove(int expectedToMove) {
+ this.expectedToMove = expectedToMove;
+ return this;
+ }
+
+ public ResponderLocationBuilder setFloorNumber(double floorNumber) {
+ this.floorNumber = floorNumber;
+ return this;
+ }
+
+ public ResponderLocation build() {
+ ResponderLocation result = Shadow.newInstanceOf(ResponderLocation.class);
+
+ ResponderLocationReflector locationResponderReflector =
+ reflector(ResponderLocationReflector.class, result);
+
+ locationResponderReflector.setAltitude(this.altitude == null ? 0 : this.altitude);
+ locationResponderReflector.setAltitudeType(this.altitudeType == null ? 0 : this.altitudeType);
+ locationResponderReflector.setAltitudeUncertainty(
+ this.altitudeUncertainty == null ? 0 : this.altitudeUncertainty);
+ locationResponderReflector.setLatitude(this.latitudeDegrees == null ? 0 : this.latitudeDegrees);
+ locationResponderReflector.setLatitudeUncertainty(
+ this.latitudeUncertainty == null ? 0 : this.latitudeUncertainty);
+ locationResponderReflector.setLongitude(
+ this.longitudeDegrees == null ? 0 : this.longitudeDegrees);
+ locationResponderReflector.setLongitudeUncertainty(
+ this.longitudeUncertainty == null ? 0 : this.longitudeUncertainty);
+ locationResponderReflector.setDatum(this.datum == null ? 0 : this.datum);
+ locationResponderReflector.setLciVersion(this.lciVersion == null ? 0 : this.lciVersion);
+ locationResponderReflector.setLciRegisteredLocationAgreement(
+ this.lciRegisteredLocationAgreement != null && this.lciRegisteredLocationAgreement);
+ locationResponderReflector.setHeightAboveFloorMeters(
+ this.heightAboveFloorMeters == null ? 0 : this.heightAboveFloorMeters);
+ locationResponderReflector.setHeightAboveFloorUncertaintyMeters(
+ this.heightAboveFloorUncertaintyMeters == null
+ ? 0
+ : this.heightAboveFloorUncertaintyMeters);
+ locationResponderReflector.setExpectedToMove(
+ this.expectedToMove == null ? 0 : this.expectedToMove);
+ locationResponderReflector.setFloorNumber(this.floorNumber == null ? 0 : this.floorNumber);
+
+ locationResponderReflector.setIsLciValid(
+ this.altitude != null
+ && this.latitudeDegrees != null
+ && this.latitudeUncertainty != null
+ && this.longitudeDegrees != null
+ && this.longitudeUncertainty != null
+ && this.datum != null
+ && this.lciVersion != null
+ && this.lciRegisteredLocationAgreement != null
+ && this.altitudeType != null);
+
+ locationResponderReflector.setIsZValid(
+ this.heightAboveFloorMeters != null
+ && this.floorNumber != null
+ && this.expectedToMove != null
+ && this.heightAboveFloorUncertaintyMeters != null);
+
+ return result;
+ }
+
+ @ForType(ResponderLocation.class)
+ interface ResponderLocationReflector {
+
+ @Accessor("mAltitude")
+ void setAltitude(double altitude);
+
+ @Accessor("mAltitudeUncertainty")
+ void setAltitudeUncertainty(double altitudeUncertainty);
+
+ @Accessor("mAltitudeType")
+ void setAltitudeType(int altitudeType);
+
+ @Accessor("mLatitude")
+ void setLatitude(double latitudeDegrees);
+
+ @Accessor("mLatitudeUncertainty")
+ void setLatitudeUncertainty(double latitudeUncertainty);
+
+ @Accessor("mLongitude")
+ void setLongitude(double longitudeDegrees);
+
+ @Accessor("mLongitudeUncertainty")
+ void setLongitudeUncertainty(double longitudeUncertainty);
+
+ @Accessor("mDatum")
+ void setDatum(int datum);
+
+ @Accessor("mLciVersion")
+ void setLciVersion(int lciVersion);
+
+ @Accessor("mLciRegisteredLocationAgreement")
+ void setLciRegisteredLocationAgreement(boolean lciRegisteredLocationAgreement);
+
+ @Accessor("mHeightAboveFloorMeters")
+ void setHeightAboveFloorMeters(double heightAboveFloorMeters);
+
+ @Accessor("mHeightAboveFloorUncertaintyMeters")
+ void setHeightAboveFloorUncertaintyMeters(double heightAboveFloorUncertaintyMeters);
+
+ @Accessor("mExpectedToMove")
+ void setExpectedToMove(int expectedToMove);
+
+ @Accessor("mFloorNumber")
+ void setFloorNumber(double floorNumber);
+
+ @Accessor("mIsLciValid")
+ void setIsLciValid(boolean isLciValid);
+
+ @Accessor("mIsZValid")
+ void setIsZValid(boolean isZValid);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbstractCursor.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbstractCursor.java
deleted file mode 100644
index 24e384571..000000000
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAbstractCursor.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.robolectric.shadows;
-
-import android.database.AbstractCursor;
-import android.net.Uri;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.util.ReflectionHelpers;
-
-@Implements(AbstractCursor.class)
-public class ShadowAbstractCursor {
-
- @RealObject
- private AbstractCursor realAbstractCursor;
-
- /**
- * Returns the Uri set by {@code setNotificationUri()}.
- *
- * @return Notification URI.
- */
- public Uri getNotificationUri_Compatibility() {
- return ReflectionHelpers.getField(realAbstractCursor, "mNotifyUri");
- }
-} \ No newline at end of file
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
index bd9f509c1..a27d01c98 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
@@ -416,7 +416,7 @@ public class ShadowAccessibilityNodeInfo {
if (this.traversalBefore != null) {
this.traversalBefore.recycle();
}
-
+
this.traversalBefore = obtain(info);
}
@@ -627,6 +627,7 @@ public class ShadowAccessibilityNodeInfo {
}
if (getApiLevel() >= P) {
newInfo.setTooltipText(realAccessibilityNodeInfo.getTooltipText());
+ newInfo.setPaneTitle(realAccessibilityNodeInfo.getPaneTitle());
}
return newInfo;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java
index b43631c11..ea551d8c1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAlarmManager.java
@@ -1,40 +1,53 @@
package org.robolectric.shadows;
import static android.app.AlarmManager.RTC_WAKEUP;
-import static android.os.Build.VERSION_CODES.KITKAT;
-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.S;
import static org.robolectric.util.reflector.Reflector.reflector;
-import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.AlarmManager.AlarmClockInfo;
import android.app.AlarmManager.OnAlarmListener;
import android.app.PendingIntent;
-import android.content.Intent;
+import android.app.PendingIntent.CanceledException;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
import android.os.Handler;
-import java.util.Collections;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.WorkSource;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
+import java.util.PriorityQueue;
import java.util.TimeZone;
-import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
-import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
-@SuppressWarnings({"UnusedDeclaration"})
+/** Shadow for {@link android.app.AlarmManager}. */
@Implements(AlarmManager.class)
public class ShadowAlarmManager {
+ public static final long WINDOW_EXACT = 0;
+ public static final long WINDOW_HEURISTIC = -1;
+
private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getDefault();
private static boolean canScheduleExactAlarms;
- private final List<ScheduledAlarm> scheduledAlarms = new CopyOnWriteArrayList<>();
+ private static boolean autoSchedule;
+
+ private final Handler schedulingHandler = new Handler(Looper.getMainLooper());
+
+ @GuardedBy("scheduledAlarms")
+ private final PriorityQueue<InternalScheduledAlarm> scheduledAlarms = new PriorityQueue<>();
@RealObject private AlarmManager realObject;
@@ -42,267 +55,605 @@ public class ShadowAlarmManager {
public static void reset() {
TimeZone.setDefault(DEFAULT_TIMEZONE);
canScheduleExactAlarms = false;
+ autoSchedule = false;
}
- @Implementation
- protected void setTimeZone(String timeZone) {
- // Do the real check first
- reflector(AlarmManagerReflector.class, realObject).setTimeZone(timeZone);
- // Then do the right side effect
- TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
+ /**
+ * When set to true, automatically schedules alarms to fire at the appropriate time (with respect
+ * to the main Looper time) when they are set. This means that a test as below could be expected
+ * to pass:
+ *
+ * <pre>{@code
+ * shadowOf(alarmManager).setAutoSchedule(true);
+ * AlarmManager.OnAlarmListener listener = mock(AlarmManager.OnAlarmListener.class);
+ * alarmManager.setExact(
+ * ELAPSED_REALTIME_WAKEUP,
+ * SystemClock.elapsedRealtime() + 10,
+ * "tag",
+ * listener,
+ * new Handler(Looper.getMainLooper()));
+ * shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(10));
+ * verify(listener).onAlarm();
+ * }</pre>
+ *
+ * <p>Alarms are always scheduled with respect to the trigger/window start time - there is no
+ * emulation of alarms being reordered, rescheduled, or delayed, as might happen on a real device.
+ * If emulating this is necessary, see {@link #fireAlarm(ScheduledAlarm)}.
+ *
+ * <p>{@link AlarmManager.OnAlarmListener} alarms will be run on the correct Handler/Executor as
+ * specified when the alarm is set.
+ */
+ public static void setAutoSchedule(boolean autoSchedule) {
+ ShadowAlarmManager.autoSchedule = autoSchedule;
}
@Implementation
- protected void set(int type, long triggerAtTime, PendingIntent operation) {
- internalSet(type, triggerAtTime, 0L, operation, null);
+ protected void set(int type, long triggerAtMs, PendingIntent operation) {
+ setImpl(type, triggerAtMs, WINDOW_HEURISTIC, 0L, operation, null, null, false);
}
- @Implementation(minSdk = N)
+ @Implementation(minSdk = VERSION_CODES.N)
protected void set(
- int type, long triggerAtTime, String tag, OnAlarmListener listener, Handler targetHandler) {
- internalSet(type, triggerAtTime, listener, targetHandler);
+ int type,
+ long triggerAtMs,
+ @Nullable String tag,
+ OnAlarmListener listener,
+ @Nullable Handler handler) {
+ setImpl(
+ type,
+ triggerAtMs,
+ WINDOW_HEURISTIC,
+ 0L,
+ tag,
+ listener,
+ new HandlerExecutor(handler),
+ null,
+ false);
}
- @Implementation(minSdk = KITKAT)
- protected void setExact(int type, long triggerAtTime, PendingIntent operation) {
- internalSet(type, triggerAtTime, 0L, operation, null);
+ @Implementation
+ protected void setRepeating(
+ int type, long triggerAtMs, long intervalMs, PendingIntent operation) {
+ setImpl(type, triggerAtMs, WINDOW_HEURISTIC, intervalMs, operation, null, null, false);
}
- @Implementation(minSdk = N)
- protected void setExact(
- int type, long triggerAtTime, String tag, OnAlarmListener listener, Handler targetHandler) {
- internalSet(type, triggerAtTime, listener, targetHandler);
+ @Implementation(minSdk = VERSION_CODES.KITKAT)
+ protected void setWindow(
+ int type, long windowStartMs, long windowLengthMs, PendingIntent operation) {
+ setImpl(type, windowStartMs, windowLengthMs, 0L, operation, null, null, false);
}
- @Implementation(minSdk = KITKAT)
+ @Implementation(minSdk = VERSION_CODES.N)
protected void setWindow(
- int type, long windowStartMillis, long windowLengthMillis, PendingIntent operation) {
- internalSet(type, windowStartMillis, 0L, operation, null);
+ int type,
+ long windowStartMs,
+ long windowLengthMs,
+ @Nullable String tag,
+ OnAlarmListener listener,
+ @Nullable Handler handler) {
+ setImpl(
+ type,
+ windowStartMs,
+ windowLengthMs,
+ 0L,
+ tag,
+ listener,
+ new HandlerExecutor(handler),
+ null,
+ false);
+ }
+
+ @Implementation(minSdk = 34)
+ protected void setWindow(
+ int type,
+ long windowStartMs,
+ long windowLengthMs,
+ @Nullable String tag,
+ Executor executor,
+ OnAlarmListener listener) {
+ setImpl(type, windowStartMs, windowLengthMs, 0L, tag, listener, executor, null, false);
}
- @Implementation(minSdk = N)
+ @Implementation(minSdk = 34)
protected void setWindow(
int type,
- long windowStartMillis,
- long windowLengthMillis,
- String tag,
+ long windowStartMs,
+ long windowLengthMs,
+ @Nullable String tag,
+ Executor executor,
+ WorkSource workSource,
+ OnAlarmListener listener) {
+ setImpl(type, windowStartMs, windowLengthMs, 0L, tag, listener, executor, workSource, false);
+ }
+
+ @Implementation(minSdk = VERSION_CODES.S)
+ protected void setPrioritized(
+ int type,
+ long windowStartMs,
+ long windowLengthMs,
+ @Nullable String tag,
+ Executor executor,
+ OnAlarmListener listener) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+ setImpl(type, windowStartMs, windowLengthMs, 0L, tag, listener, executor, null, true);
+ }
+
+ @Implementation(minSdk = VERSION_CODES.KITKAT)
+ protected void setExact(int type, long triggerAtMs, PendingIntent operation) {
+ setImpl(type, triggerAtMs, WINDOW_EXACT, 0L, operation, null, null, false);
+ }
+
+ @Implementation(minSdk = VERSION_CODES.N)
+ protected void setExact(
+ int type,
+ long triggerAtTime,
+ @Nullable String tag,
OnAlarmListener listener,
- Handler targetHandler) {
- internalSet(type, windowStartMillis, listener, targetHandler);
+ @Nullable Handler targetHandler) {
+ setImpl(
+ type,
+ triggerAtTime,
+ WINDOW_EXACT,
+ 0L,
+ tag,
+ listener,
+ new HandlerExecutor(targetHandler),
+ null,
+ false);
+ }
+
+ @RequiresApi(VERSION_CODES.LOLLIPOP)
+ @Implementation(minSdk = VERSION_CODES.LOLLIPOP)
+ protected void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
+ setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0L, operation, null, info, true);
}
- @Implementation(minSdk = M)
- protected void setAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) {
- internalSet(type, triggerAtTime, 0L, operation, null, true);
+ @Implementation(minSdk = VERSION_CODES.KITKAT)
+ protected void set(
+ int type,
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
+ PendingIntent operation,
+ @Nullable WorkSource workSource) {
+ setImpl(type, triggerAtMs, windowLengthMs, intervalMs, operation, workSource, null, false);
}
- @Implementation(minSdk = M)
- protected void setExactAndAllowWhileIdle(int type, long triggerAtTime, PendingIntent operation) {
- internalSet(type, triggerAtTime, 0L, operation, null, true);
+ @Implementation(minSdk = VERSION_CODES.N)
+ protected void set(
+ int type,
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
+ @Nullable String tag,
+ OnAlarmListener listener,
+ @Nullable Handler targetHandler,
+ @Nullable WorkSource workSource) {
+ setImpl(
+ type,
+ triggerAtMs,
+ windowLengthMs,
+ intervalMs,
+ tag,
+ listener,
+ new HandlerExecutor(targetHandler),
+ workSource,
+ false);
}
- @Implementation
- protected void setRepeating(
- int type, long triggerAtTime, long interval, PendingIntent operation) {
- internalSet(type, triggerAtTime, interval, operation, null);
+ @Implementation(minSdk = VERSION_CODES.N)
+ protected void set(
+ int type,
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
+ OnAlarmListener listener,
+ @Nullable Handler targetHandler,
+ @Nullable WorkSource workSource) {
+ setImpl(
+ type,
+ triggerAtMs,
+ windowLengthMs,
+ intervalMs,
+ null,
+ listener,
+ new HandlerExecutor(targetHandler),
+ workSource,
+ false);
+ }
+
+ @Implementation(minSdk = VERSION_CODES.S)
+ protected void setExact(
+ int type,
+ long triggerAtMs,
+ @Nullable String tag,
+ Executor executor,
+ WorkSource workSource,
+ OnAlarmListener listener) {
+ Objects.requireNonNull(workSource);
+ setImpl(type, triggerAtMs, WINDOW_EXACT, 0L, tag, listener, executor, workSource, false);
}
@Implementation
protected void setInexactRepeating(
- int type, long triggerAtMillis, long intervalMillis, PendingIntent operation) {
- internalSet(type, triggerAtMillis, intervalMillis, operation, null);
+ int type, long triggerAtMs, long intervalMillis, PendingIntent operation) {
+ setImpl(type, triggerAtMs, WINDOW_HEURISTIC, intervalMillis, operation, null, null, false);
}
- @Implementation(minSdk = LOLLIPOP)
- protected void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {
- internalSet(RTC_WAKEUP, info.getTriggerTime(), 0L, operation, info.getShowIntent());
+ @Implementation(minSdk = VERSION_CODES.M)
+ protected void setAndAllowWhileIdle(int type, long triggerAtMs, PendingIntent operation) {
+ setImpl(type, triggerAtMs, WINDOW_HEURISTIC, 0L, operation, null, null, true);
}
- @Implementation(minSdk = LOLLIPOP)
- protected AlarmClockInfo getNextAlarmClock() {
- for (ScheduledAlarm scheduledAlarm : scheduledAlarms) {
- AlarmClockInfo alarmClockInfo = scheduledAlarm.getAlarmClockInfo();
- if (alarmClockInfo != null) {
- return alarmClockInfo;
+ @Implementation(minSdk = VERSION_CODES.M)
+ protected void setExactAndAllowWhileIdle(int type, long triggerAtMs, PendingIntent operation) {
+ setImpl(type, triggerAtMs, WINDOW_EXACT, 0L, operation, null, null, true);
+ }
+
+ @Implementation(minSdk = 34)
+ protected void setExactAndAllowWhileIdle(
+ int type,
+ long triggerAtMs,
+ @Nullable String tag,
+ Executor executor,
+ @Nullable WorkSource workSource,
+ OnAlarmListener listener) {
+ setImpl(type, triggerAtMs, WINDOW_EXACT, 0L, tag, listener, executor, workSource, true);
+ }
+
+ @Implementation
+ protected void cancel(PendingIntent operation) {
+ synchronized (scheduledAlarms) {
+ Iterables.removeIf(
+ scheduledAlarms,
+ alarm -> {
+ if (operation.equals(alarm.operation)) {
+ alarm.deschedule();
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ @Implementation(minSdk = VERSION_CODES.N)
+ protected void cancel(OnAlarmListener listener) {
+ synchronized (scheduledAlarms) {
+ Iterables.removeIf(
+ scheduledAlarms,
+ alarm -> {
+ if (listener.equals(alarm.onAlarmListener)) {
+ alarm.deschedule();
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ @Implementation(minSdk = 34)
+ protected void cancelAll() {
+ synchronized (scheduledAlarms) {
+ for (InternalScheduledAlarm alarm : scheduledAlarms) {
+ alarm.deschedule();
}
+ scheduledAlarms.clear();
}
- return null;
}
- private void internalSet(
- int type,
- long triggerAtTime,
- long interval,
- PendingIntent operation,
- PendingIntent showIntent) {
- cancel(operation);
+ @Implementation
+ protected void setTimeZone(String timeZone) {
+ // Do the real check first
+ reflector(AlarmManagerReflector.class, realObject).setTimeZone(timeZone);
+ // Then do the right side effect
+ TimeZone.setDefault(TimeZone.getTimeZone(timeZone));
+ }
+
+ @Implementation(minSdk = VERSION_CODES.S)
+ protected boolean canScheduleExactAlarms() {
+ return canScheduleExactAlarms;
+ }
+
+ @RequiresApi(VERSION_CODES.LOLLIPOP)
+ @Implementation(minSdk = VERSION_CODES.LOLLIPOP)
+ @Nullable
+ protected AlarmClockInfo getNextAlarmClock() {
synchronized (scheduledAlarms) {
- scheduledAlarms.add(new ScheduledAlarm(type, triggerAtTime, interval, operation, showIntent));
- Collections.sort(scheduledAlarms);
+ for (ScheduledAlarm scheduledAlarm : scheduledAlarms) {
+ AlarmClockInfo alarmClockInfo = scheduledAlarm.getAlarmClockInfo();
+ if (alarmClockInfo != null) {
+ return alarmClockInfo;
+ }
+ }
+ return null;
}
}
- private void internalSet(
+ private void setImpl(
int type,
- long triggerAtTime,
- long interval,
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
PendingIntent operation,
- PendingIntent showIntent,
+ @Nullable WorkSource workSource,
+ @Nullable Object alarmClockInfo,
boolean allowWhileIdle) {
- cancel(operation);
synchronized (scheduledAlarms) {
+ cancel(operation);
scheduledAlarms.add(
- new ScheduledAlarm(type, triggerAtTime, interval, operation, showIntent, allowWhileIdle));
- Collections.sort(scheduledAlarms);
+ new InternalScheduledAlarm(
+ type,
+ triggerAtMs,
+ windowLengthMs,
+ intervalMs,
+ operation,
+ workSource,
+ alarmClockInfo,
+ allowWhileIdle)
+ .schedule());
}
}
- private void internalSet(
- int type, long triggerAtTime, OnAlarmListener listener, Handler handler) {
- cancel(listener);
+ private void setImpl(
+ int type,
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
+ @Nullable String tag,
+ OnAlarmListener listener,
+ Executor executor,
+ @Nullable WorkSource workSource,
+ boolean allowWhileIdle) {
synchronized (scheduledAlarms) {
- scheduledAlarms.add(new ScheduledAlarm(type, triggerAtTime, 0L, listener, handler));
- Collections.sort(scheduledAlarms);
+ cancel(listener);
+ scheduledAlarms.add(
+ new InternalScheduledAlarm(
+ type,
+ triggerAtMs,
+ windowLengthMs,
+ intervalMs,
+ tag,
+ listener,
+ executor,
+ workSource,
+ null,
+ allowWhileIdle)
+ .schedule());
}
}
- /** @return the next scheduled alarm after consuming it */
+ /**
+ * Returns the earliest scheduled alarm and removes it from the list of scheduled alarms.
+ *
+ * @deprecated Prefer to use {@link ShadowAlarmManager#setAutoSchedule(boolean)} in combination
+ * with incrementing time to actually run alarms and test their side-effects.
+ */
+ @Deprecated
+ @Nullable
public ScheduledAlarm getNextScheduledAlarm() {
- if (scheduledAlarms.isEmpty()) {
- return null;
- } else {
- return scheduledAlarms.remove(0);
+ synchronized (scheduledAlarms) {
+ InternalScheduledAlarm alarm = scheduledAlarms.poll();
+ if (alarm != null) {
+ alarm.deschedule();
+ }
+ return alarm;
}
}
- /** @return the most recently scheduled alarm without consuming it */
+ /** Returns the earliest scheduled alarm. */
+ @Nullable
public ScheduledAlarm peekNextScheduledAlarm() {
- if (scheduledAlarms.isEmpty()) {
- return null;
- } else {
- return scheduledAlarms.get(0);
+ synchronized (scheduledAlarms) {
+ return scheduledAlarms.peek();
}
}
- /** @return all scheduled alarms */
+ /** Returns a list of all scheduled alarms, ordered from earliest time to latest time. */
public List<ScheduledAlarm> getScheduledAlarms() {
- return scheduledAlarms;
- }
-
- @Implementation
- protected void cancel(PendingIntent operation) {
- ShadowPendingIntent shadowPendingIntent = Shadow.extract(operation);
- final Intent toRemove = shadowPendingIntent.getSavedIntent();
- final int requestCode = shadowPendingIntent.getRequestCode();
- for (ScheduledAlarm scheduledAlarm : scheduledAlarms) {
- if (scheduledAlarm.operation != null) {
- ShadowPendingIntent scheduledShadowPendingIntent = Shadow.extract(scheduledAlarm.operation);
- final Intent scheduledIntent = scheduledShadowPendingIntent.getSavedIntent();
- final int scheduledRequestCode = scheduledShadowPendingIntent.getRequestCode();
- if (scheduledIntent.filterEquals(toRemove) && scheduledRequestCode == requestCode) {
- scheduledAlarms.remove(scheduledAlarm);
- break;
- }
- }
+ synchronized (scheduledAlarms) {
+ return new ArrayList<>(scheduledAlarms);
}
}
- @Implementation(minSdk = N)
- protected void cancel(OnAlarmListener listener) {
- for (ScheduledAlarm scheduledAlarm : scheduledAlarms) {
- if (scheduledAlarm.onAlarmListener != null) {
- if (scheduledAlarm.onAlarmListener.equals(listener)) {
- scheduledAlarms.remove(scheduledAlarm);
- break;
- }
+ /**
+ * Immediately removes the given alarm from the list of scheduled alarms (and then reschedules it
+ * in the case of a repeating alarm) and fires it. The given alarm must on the list of scheduled
+ * alarms prior to being fired.
+ *
+ * <p>Generally prefer to use {@link ShadowAlarmManager#setAutoSchedule(boolean)} in combination
+ * with advancing time on the main Looper in order to test alarms - however this method can be
+ * useful to emulate rescheduled, reordered, or delayed alarms, as may happen on a real device.
+ */
+ public void fireAlarm(ScheduledAlarm alarm) {
+ synchronized (scheduledAlarms) {
+ if (!scheduledAlarms.contains(alarm)) {
+ throw new IllegalArgumentException();
}
- }
- }
- /** Returns the schedule exact alarm state set by {@link #setCanScheduleExactAlarms}. */
- @Implementation(minSdk = S)
- protected boolean canScheduleExactAlarms() {
- return canScheduleExactAlarms;
+ ((InternalScheduledAlarm) alarm).deschedule();
+ ((InternalScheduledAlarm) alarm).run();
+ }
}
/**
- * Sets the schedule exact alarm state reported by {@link AlarmManager#canScheduleExactAlarms},
+ * Sets the schedule exact alarm state reported by {@link AlarmManager#canScheduleExactAlarms()},
* but has no effect otherwise.
*/
public static void setCanScheduleExactAlarms(boolean scheduleExactAlarms) {
canScheduleExactAlarms = scheduleExactAlarms;
}
- /** Container object to hold a PendingIntent and parameters describing when to send it. */
+ /** Represents a set alarm. */
public static class ScheduledAlarm implements Comparable<ScheduledAlarm> {
- public final int type;
- public final long triggerAtTime;
- public final long interval;
- public final PendingIntent operation;
- public final boolean allowWhileIdle;
-
- // A non-null showIntent implies this alarm has a user interface. (i.e. in an alarm clock app)
- public final PendingIntent showIntent;
-
- public final OnAlarmListener onAlarmListener;
- public final Handler handler;
-
+ @Deprecated public final int type;
+ @Deprecated public final long triggerAtTime;
+ private final long windowLengthMs;
+ @Deprecated public final long interval;
+ @Nullable private final String tag;
+ @Deprecated @Nullable public final PendingIntent operation;
+ @Deprecated @Nullable public final OnAlarmListener onAlarmListener;
+ @Deprecated @Nullable public final Executor executor;
+ @Nullable private final WorkSource workSource;
+ @Nullable private final Object alarmClockInfo;
+ @Deprecated public final boolean allowWhileIdle;
+
+ @Deprecated @Nullable public final PendingIntent showIntent;
+ @Deprecated @Nullable public final Handler handler;
+
+ @Deprecated
public ScheduledAlarm(
- int type, long triggerAtTime, PendingIntent operation, PendingIntent showIntent) {
- this(type, triggerAtTime, 0, operation, showIntent);
+ int type, long triggerAtMs, PendingIntent operation, PendingIntent showIntent) {
+ this(type, triggerAtMs, 0, operation, showIntent);
}
+ @Deprecated
public ScheduledAlarm(
int type,
- long triggerAtTime,
- long interval,
+ long triggerAtMs,
+ long intervalMs,
PendingIntent operation,
PendingIntent showIntent) {
- this(type, triggerAtTime, interval, operation, showIntent, null, null, false);
+ this(type, triggerAtMs, intervalMs, operation, showIntent, false);
}
+ @Deprecated
public ScheduledAlarm(
int type,
- long triggerAtTime,
- long interval,
+ long triggerAtMs,
+ long intervalMs,
PendingIntent operation,
PendingIntent showIntent,
boolean allowWhileIdle) {
- this(type, triggerAtTime, interval, operation, showIntent, null, null, allowWhileIdle);
+ this(
+ type,
+ triggerAtMs,
+ intervalMs,
+ WINDOW_HEURISTIC,
+ operation,
+ null,
+ VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && showIntent != null
+ ? new AlarmClockInfo(triggerAtMs, showIntent)
+ : null,
+ allowWhileIdle);
}
- private ScheduledAlarm(
+ protected ScheduledAlarm(
int type,
- long triggerAtTime,
- long interval,
- OnAlarmListener onAlarmListener,
- Handler handler) {
- this(type, triggerAtTime, interval, null, null, onAlarmListener, handler, false);
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
+ PendingIntent operation,
+ @Nullable WorkSource workSource,
+ @Nullable Object alarmClockInfo,
+ boolean allowWhileIdle) {
+ this.type = type;
+ this.triggerAtTime = triggerAtMs;
+ this.windowLengthMs = windowLengthMs;
+ this.interval = intervalMs;
+ this.tag = null;
+ this.operation = Objects.requireNonNull(operation);
+ this.onAlarmListener = null;
+ this.executor = null;
+ this.workSource = workSource;
+ this.alarmClockInfo = alarmClockInfo;
+ this.allowWhileIdle = allowWhileIdle;
+
+ this.handler = null;
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && alarmClockInfo != null) {
+ this.showIntent = ((AlarmClockInfo) alarmClockInfo).getShowIntent();
+ } else {
+ this.showIntent = null;
+ }
}
- private ScheduledAlarm(
+ protected ScheduledAlarm(
int type,
- long triggerAtTime,
- long interval,
- PendingIntent operation,
- PendingIntent showIntent,
- OnAlarmListener onAlarmListener,
- Handler handler,
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
+ @Nullable String tag,
+ OnAlarmListener listener,
+ Executor executor,
+ @Nullable WorkSource workSource,
+ @Nullable Object alarmClockInfo,
boolean allowWhileIdle) {
this.type = type;
- this.triggerAtTime = triggerAtTime;
- this.operation = operation;
- this.interval = interval;
- this.showIntent = showIntent;
- this.onAlarmListener = onAlarmListener;
- this.handler = handler;
+ this.triggerAtTime = triggerAtMs;
+ this.windowLengthMs = windowLengthMs;
+ this.interval = intervalMs;
+ this.tag = tag;
+ this.operation = null;
+ this.onAlarmListener = Objects.requireNonNull(listener);
+ this.executor = Objects.requireNonNull(executor);
+ this.workSource = workSource;
+ this.alarmClockInfo = alarmClockInfo;
this.allowWhileIdle = allowWhileIdle;
+
+ if (executor instanceof HandlerExecutor) {
+ this.handler = ((HandlerExecutor) executor).handler;
+ } else {
+ this.handler = null;
+ }
+ if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && alarmClockInfo != null) {
+ this.showIntent = ((AlarmClockInfo) alarmClockInfo).getShowIntent();
+ } else {
+ this.showIntent = null;
+ }
}
- @TargetApi(LOLLIPOP)
+ protected ScheduledAlarm(long triggerAtMs, ScheduledAlarm alarm) {
+ this.type = alarm.type;
+ this.triggerAtTime = triggerAtMs;
+ this.windowLengthMs = alarm.windowLengthMs;
+ this.interval = alarm.interval;
+ this.tag = alarm.tag;
+ this.operation = alarm.operation;
+ this.onAlarmListener = alarm.onAlarmListener;
+ this.executor = alarm.executor;
+ this.workSource = alarm.workSource;
+ this.alarmClockInfo = alarm.alarmClockInfo;
+ this.allowWhileIdle = alarm.allowWhileIdle;
+
+ this.handler = alarm.handler;
+ this.showIntent = alarm.showIntent;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public long getTriggerAtMs() {
+ return triggerAtTime;
+ }
+
+ public long getWindowLengthMs() {
+ return windowLengthMs;
+ }
+
+ public long getIntervalMs() {
+ return interval;
+ }
+
+ @Nullable
+ public String getTag() {
+ return tag;
+ }
+
+ @Nullable
+ public WorkSource getWorkSource() {
+ return workSource;
+ }
+
+ @RequiresApi(VERSION_CODES.LOLLIPOP)
+ @Nullable
public AlarmClockInfo getAlarmClockInfo() {
- return showIntent == null ? null : new AlarmClockInfo(triggerAtTime, showIntent);
+ return (AlarmClockInfo) alarmClockInfo;
+ }
+
+ public boolean isAllowWhileIdle() {
+ return allowWhileIdle;
}
@Override
@@ -311,6 +662,119 @@ public class ShadowAlarmManager {
}
}
+ // wrapper class created because we can't modify ScheduledAlarm without breaking compatibility
+ private class InternalScheduledAlarm extends ScheduledAlarm implements Runnable {
+
+ InternalScheduledAlarm(
+ int type,
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
+ PendingIntent operation,
+ @Nullable WorkSource workSource,
+ @Nullable Object alarmClockInfo,
+ boolean allowWhileIdle) {
+ super(
+ type,
+ triggerAtMs,
+ windowLengthMs,
+ intervalMs,
+ operation,
+ workSource,
+ alarmClockInfo,
+ allowWhileIdle);
+ }
+
+ InternalScheduledAlarm(
+ int type,
+ long triggerAtMs,
+ long windowLengthMs,
+ long intervalMs,
+ @Nullable String tag,
+ OnAlarmListener listener,
+ Executor executor,
+ @Nullable WorkSource workSource,
+ @Nullable Object alarmClockInfo,
+ boolean allowWhileIdle) {
+ super(
+ type,
+ triggerAtMs,
+ windowLengthMs,
+ intervalMs,
+ tag,
+ listener,
+ executor,
+ workSource,
+ alarmClockInfo,
+ allowWhileIdle);
+ }
+
+ InternalScheduledAlarm(long triggerAtMs, InternalScheduledAlarm alarm) {
+ super(triggerAtMs, alarm);
+ }
+
+ InternalScheduledAlarm schedule() {
+ if (autoSchedule) {
+ schedulingHandler.postDelayed(this, triggerAtTime - SystemClock.elapsedRealtime());
+ }
+ return this;
+ }
+
+ void deschedule() {
+ schedulingHandler.removeCallbacks(this);
+ }
+
+ @Override
+ public void run() {
+ Executor executor;
+ if (operation != null) {
+ executor = Runnable::run;
+ } else {
+ executor = Objects.requireNonNull(this.executor);
+ }
+
+ executor.execute(
+ () -> {
+ synchronized (scheduledAlarms) {
+ if (!scheduledAlarms.remove(this)) {
+ return;
+ }
+ if (interval > 0) {
+ scheduledAlarms.add(
+ new InternalScheduledAlarm(triggerAtTime + interval, this).schedule());
+ }
+ }
+ if (operation != null) {
+ try {
+ operation.send();
+ } catch (CanceledException e) {
+ // only necessary in case this is a repeated alarm and we've already rescheduled
+ cancel(operation);
+ }
+ } else if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ Objects.requireNonNull(onAlarmListener).onAlarm();
+ } else {
+ throw new IllegalStateException();
+ }
+ });
+ }
+ }
+
+ private static final class HandlerExecutor implements Executor {
+ private final Handler handler;
+
+ HandlerExecutor(@Nullable Handler handler) {
+ this.handler = handler != null ? handler : new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ if (!handler.post(command)) {
+ throw new RejectedExecutionException(handler + " is shutting down");
+ }
+ }
+ }
+
@ForType(AlarmManager.class)
interface AlarmManagerReflector {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java
index 8666c6b8e..10e293758 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAnimationUtils.java
@@ -9,7 +9,6 @@ import android.view.animation.LinearInterpolator;
import android.view.animation.TranslateAnimation;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-import org.robolectric.shadow.api.Shadow;
@SuppressWarnings({"UnusedDeclaration"})
@Implements(AnimationUtils.class)
@@ -24,8 +23,6 @@ public class ShadowAnimationUtils {
protected static LayoutAnimationController loadLayoutAnimation(Context context, int id) {
Animation anim = new TranslateAnimation(0, 0, 30, 0);
LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
- ShadowLayoutAnimationController shadowLayoutAnimationController = Shadow.extract(layoutAnim);
- shadowLayoutAnimationController.setLoadedFromResourceId(id);
return layoutAnim;
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java
index cf93b9e9c..a8a0847a0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppWidgetManager.java
@@ -55,6 +55,10 @@ public class ShadowAppWidgetManager {
private Multimap<UserHandle, AppWidgetProviderInfo> installedProvidersForProfile =
HashMultimap.create();
+ // AppWidgetProvider is enabled if at least one widget is active. `isWidgetsEnabled` should be set
+ // to false if the last widget is removed (when removing widgets is implemented).
+ private boolean isWidgetsEnabled = false;
+
@Implementation(maxSdk = KITKAT)
protected void __constructor__(Context context) {
this.context = context;
@@ -307,6 +311,15 @@ public class ShadowAppWidgetManager {
widgetInfo.view = widgetInfo.lastRemoteViews.apply(context, new AppWidgetHostView(context));
}
+ private void enableWidgetsIfNecessary(Class<? extends AppWidgetProvider> appWidgetProviderClass) {
+ if (!isWidgetsEnabled) {
+ isWidgetsEnabled = true;
+ AppWidgetProvider appWidgetProvider =
+ ReflectionHelpers.callConstructor(appWidgetProviderClass);
+ appWidgetProvider.onReceive(context, new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED));
+ }
+ }
+
/**
* Creates a widget by inflating its layout.
*
@@ -345,7 +358,12 @@ public class ShadowAppWidgetManager {
newWidgetIds[i] = myWidgetId;
}
- appWidgetProvider.onUpdate(context, realAppWidgetManager, newWidgetIds);
+ // Enable widgets if we are creating the first widget.
+ enableWidgetsIfNecessary(appWidgetProviderClass);
+
+ Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, newWidgetIds);
+ appWidgetProvider.onReceive(context, intent);
return newWidgetIds;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
index 544d1999c..ae8dfb894 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
@@ -67,6 +67,7 @@ import android.content.pm.ModuleInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.OnPermissionsChangedListener;
import android.content.pm.PackageManager.PackageInfoFlags;
@@ -140,6 +141,15 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
@Implementation
public List<PackageInfo> getInstalledPackages(int flags) {
+ return getInstalledPackages((long) flags);
+ }
+
+ @Implementation(minSdk = TIRAMISU)
+ protected List<PackageInfo> getInstalledPackages(Object flags) {
+ return getInstalledPackages(((PackageInfoFlags) flags).getValue());
+ }
+
+ private List<PackageInfo> getInstalledPackages(long flags) {
List<PackageInfo> result = new ArrayList<>();
synchronized (lock) {
Set<String> packageNames = null;
@@ -429,6 +439,16 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
@Implementation
protected PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
+ return getPackageInfo(packageName, (long) flags);
+ }
+
+ @Implementation(minSdk = TIRAMISU)
+ protected PackageInfo getPackageInfo(Object packageName, Object flags)
+ throws NameNotFoundException {
+ return getPackageInfo((String) packageName, ((PackageInfoFlags) flags).getValue());
+ }
+
+ private PackageInfo getPackageInfo(String packageName, long flags) throws NameNotFoundException {
synchronized (lock) {
PackageInfo info = packageInfos.get(packageName);
if (info == null
@@ -494,7 +514,7 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
}
private <T extends ComponentInfo> T[] applyFlagsToComponentInfoList(
- T[] components, int flags, int activationFlag, Function<T, T> copyConstructor) {
+ T[] components, long flags, int activationFlag, Function<T, T> copyConstructor) {
if (components == null || (flags & activationFlag) == 0) {
return null;
}
@@ -536,7 +556,7 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
|| (VERSION.SDK_INT >= VERSION_CODES.KITKAT && resolveInfo.providerInfo != null);
}
- private static boolean isFlagSet(int flags, int matchFlag) {
+ private static boolean isFlagSet(long flags, long matchFlag) {
return (flags & matchFlag) == matchFlag;
}
@@ -859,6 +879,11 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
ActivityInfo::new);
}
+ @Implementation(minSdk = TIRAMISU)
+ protected List<ResolveInfo> queryBroadcastReceivers(Object intent, @NonNull Object flags) {
+ return queryBroadcastReceivers((Intent) intent, (int) ((ResolveInfoFlags) flags).getValue());
+ }
+
private static int matchIntentFilter(Intent intent, IntentFilter intentFilter) {
return intentFilter.match(
intent.getAction(),
@@ -891,7 +916,7 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
*
* @throws NameNotFoundException when component is filtered out by a flag
*/
- private void applyFlagsToComponentInfo(ComponentInfo componentInfo, int flags)
+ private void applyFlagsToComponentInfo(ComponentInfo componentInfo, long flags)
throws NameNotFoundException {
componentInfo.name = (componentInfo.name == null) ? "" : componentInfo.name;
ApplicationInfo applicationInfo = componentInfo.applicationInfo;
@@ -975,6 +1000,15 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
@Implementation
protected List<ApplicationInfo> getInstalledApplications(int flags) {
+ return getInstalledApplications((long) flags);
+ }
+
+ @Implementation(minSdk = TIRAMISU)
+ protected List<ApplicationInfo> getInstalledApplications(Object flags) {
+ return getInstalledApplications(((ApplicationInfoFlags) flags).getValue());
+ }
+
+ private List<ApplicationInfo> getInstalledApplications(long flags) {
List<PackageInfo> packageInfos = getInstalledPackages(flags);
List<ApplicationInfo> result = new ArrayList<>(packageInfos.size());
@@ -1305,6 +1339,11 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
return uid;
}
+ @Implementation(minSdk = TIRAMISU)
+ protected Object getPackageUid(Object packageName, Object flags) throws NameNotFoundException {
+ return getPackageUid((String) packageName, (int) ((PackageInfoFlags) flags).getValue());
+ }
+
@Implementation(minSdk = N)
protected int getPackageUidAsUser(String packageName, int userId) throws NameNotFoundException {
return 0;
@@ -1354,7 +1393,7 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
return packageInfo.applicationInfo;
}
- private void applyFlagsToApplicationInfo(@Nullable ApplicationInfo appInfo, int flags)
+ private void applyFlagsToApplicationInfo(@Nullable ApplicationInfo appInfo, long flags)
throws NameNotFoundException {
if (appInfo == null) {
return;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
index 183e9f38d..223435110 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
@@ -14,6 +14,7 @@ import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.TargetApi;
import android.media.AudioAttributes;
+import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
@@ -74,6 +75,7 @@ public class ShadowAudioManager {
new HashSet<>();
private final HashSet<AudioManager.AudioPlaybackCallback> audioPlaybackCallbacks =
new HashSet<>();
+ private final HashSet<AudioDeviceCallback> audioDeviceCallbacks = new HashSet<>();
private int ringerMode = AudioManager.RINGER_MODE_NORMAL;
private int mode = AudioManager.MODE_NORMAL;
private boolean bluetoothA2dpOn;
@@ -418,12 +420,123 @@ public class ShadowAudioManager {
defaultDevicesForAttributes = devices;
}
+ /**
+ * Sets the list of connected input devices represented by {@link AudioDeviceInfo}.
+ *
+ * <p>The previous list of input devices is replaced and no notifications of the list of {@link
+ * AudioDeviceCallback} is done.
+ *
+ * <p>To add/remove devices one by one and trigger notifications for the list of {@link
+ * AudioDeviceCallback} please use one of the following methods {@link
+ * #addInputDevice(AudioDeviceInfo, boolean)}, {@link #removeInputDevice(AudioDeviceInfo,
+ * boolean)}.
+ */
public void setInputDevices(List<AudioDeviceInfo> inputDevices) {
- this.inputDevices = inputDevices;
+ this.inputDevices = new ArrayList<>(inputDevices);
}
+ /**
+ * Sets the list of connected output devices represented by {@link AudioDeviceInfo}.
+ *
+ * <p>The previous list of output devices is replaced and no notifications of the list of {@link
+ * AudioDeviceCallback} is done.
+ *
+ * <p>To add/remove devices one by one and trigger notifications for the list of {@link
+ * AudioDeviceCallback} please use one of the following methods {@link
+ * #addOutputDevice(AudioDeviceInfo, boolean)}, {@link #removeOutputDevice(AudioDeviceInfo,
+ * boolean)}.
+ */
public void setOutputDevices(List<AudioDeviceInfo> outputDevices) {
- this.outputDevices = outputDevices;
+ this.outputDevices = new ArrayList<>(outputDevices);
+ }
+
+ /**
+ * Adds an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if
+ * the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}.
+ */
+ public void addInputDevice(AudioDeviceInfo inputDevice, boolean notifyAudioDeviceCallbacks) {
+ boolean changed =
+ !this.inputDevices.contains(inputDevice) && this.inputDevices.add(inputDevice);
+ if (changed && notifyAudioDeviceCallbacks) {
+ notifyAudioDeviceCallbacks(ImmutableList.of(inputDevice), /* added= */ true);
+ }
+ }
+
+ /**
+ * Removes an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback}
+ * if the device was present before and indicated by {@code notifyAudioDeviceCallbacks}.
+ */
+ public void removeInputDevice(AudioDeviceInfo inputDevice, boolean notifyAudioDeviceCallbacks) {
+ boolean changed = this.inputDevices.remove(inputDevice);
+ if (changed && notifyAudioDeviceCallbacks) {
+ notifyAudioDeviceCallbacks(ImmutableList.of(inputDevice), /* added= */ false);
+ }
+ }
+
+ /**
+ * Adds an output {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if
+ * the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}.
+ */
+ public void addOutputDevice(AudioDeviceInfo outputDevice, boolean notifyAudioDeviceCallbacks) {
+ boolean changed =
+ !this.outputDevices.contains(outputDevice) && this.outputDevices.add(outputDevice);
+ if (changed && notifyAudioDeviceCallbacks) {
+ notifyAudioDeviceCallbacks(ImmutableList.of(outputDevice), /* added= */ true);
+ }
+ }
+
+ /**
+ * Removes an output {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback}
+ * if the device was present before and indicated by {@code notifyAudioDeviceCallbacks}.
+ */
+ public void removeOutputDevice(AudioDeviceInfo outputDevice, boolean notifyAudioDeviceCallbacks) {
+ boolean changed = this.outputDevices.remove(outputDevice);
+ if (changed && notifyAudioDeviceCallbacks) {
+ notifyAudioDeviceCallbacks(ImmutableList.of(outputDevice), /* added= */ false);
+ }
+ }
+
+ /**
+ * Registers an {@link AudioDeviceCallback} object to receive notifications of changes to the set
+ * of connected audio devices.
+ *
+ * <p>The {@code handler} is ignored.
+ *
+ * @see #addInputDevice(AudioDeviceInfo, boolean)
+ * @see #addOutputDevice(AudioDeviceInfo, boolean)
+ * @see #removeInputDevice(AudioDeviceInfo, boolean)
+ * @see #removeOutputDevice(AudioDeviceInfo, boolean)
+ */
+ @Implementation(minSdk = M)
+ protected void registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler) {
+ audioDeviceCallbacks.add(callback);
+ // indicate currently available devices as added, similarly to MSG_DEVICES_CALLBACK_REGISTERED
+ callback.onAudioDevicesAdded(getDevices(AudioManager.GET_DEVICES_ALL));
+ }
+
+ /**
+ * Unregisters an {@link AudioDeviceCallback} object which has been previously registered to
+ * receive notifications of changes to the set of connected audio devices.
+ *
+ * @see #addInputDevice(AudioDeviceInfo, boolean)
+ * @see #addOutputDevice(AudioDeviceInfo, boolean)
+ * @see #removeInputDevice(AudioDeviceInfo, boolean)
+ * @see #removeOutputDevice(AudioDeviceInfo, boolean)
+ */
+ @Implementation(minSdk = M)
+ protected void unregisterAudioDeviceCallback(AudioDeviceCallback callback) {
+ audioDeviceCallbacks.remove(callback);
+ }
+
+ private void notifyAudioDeviceCallbacks(List<AudioDeviceInfo> devices, boolean added) {
+ AudioDeviceInfo[] devicesArray = devices.toArray(new AudioDeviceInfo[0]);
+ for (AudioDeviceCallback callback : audioDeviceCallbacks) {
+ if (added) {
+ callback.onAudioDevicesAdded(devicesArray);
+ } else {
+ callback.onAudioDevicesRemoved(devicesArray);
+ }
+ }
}
private List<AudioDeviceInfo> getInputDevices() {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java
index 2beaab0fb..38243e4a0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBackdropFrameRenderer.java
@@ -11,7 +11,6 @@ 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.Direct;
import org.robolectric.util.reflector.ForType;
/** Shadow for {@link BackdropFrameRenderer} */
@@ -49,7 +48,6 @@ public class ShadowBackdropFrameRenderer {
@ForType(BackdropFrameRenderer.class)
interface BackdropFrameRendererReflector {
- @Direct
void releaseRenderer();
@Accessor("mRenderer")
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
index 1b358395b..d6f07c0b0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmap.java
@@ -1,80 +1,16 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.M;
-import static android.os.Build.VERSION_CODES.O;
-import static android.os.Build.VERSION_CODES.Q;
-import static android.os.Build.VERSION_CODES.S;
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static java.lang.Integer.max;
-import static java.lang.Integer.min;
-
import android.graphics.Bitmap;
-import android.graphics.ColorSpace;
import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.Build;
-import android.os.Parcel;
-import android.util.DisplayMetrics;
-import java.awt.Color;
-import java.awt.Graphics2D;
-import java.awt.geom.Rectangle2D;
-import java.awt.image.BufferedImage;
-import java.awt.image.ColorModel;
-import java.awt.image.DataBufferInt;
-import java.awt.image.WritableRaster;
-import java.io.FileDescriptor;
import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.Buffer;
-import java.nio.ByteBuffer;
-import java.nio.IntBuffer;
-import java.util.Arrays;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-
-@SuppressWarnings({"UnusedDeclaration"})
-@Implements(Bitmap.class)
-public class ShadowBitmap {
- /** Number of bytes used internally to represent each pixel */
- private static final int INTERNAL_BYTES_PER_PIXEL = 4;
+import org.robolectric.shadow.api.ShadowPicker;
+import org.robolectric.shadows.ShadowBitmap.Picker;
- int createdFromResId = -1;
- String createdFromPath;
- InputStream createdFromStream;
- FileDescriptor createdFromFileDescriptor;
- byte[] createdFromBytes;
- @RealObject private Bitmap realBitmap;
- private Bitmap createdFromBitmap;
- private Bitmap scaledFromBitmap;
- private int createdFromX = -1;
- private int createdFromY = -1;
- private int createdFromWidth = -1;
- private int createdFromHeight = -1;
- private int[] createdFromColors;
- private Matrix createdFromMatrix;
- private boolean createdFromFilter;
-
- private int width;
- private int height;
- private BufferedImage bufferedImage;
- private Bitmap.Config config;
- private boolean mutable = true;
- private String description = "";
- private boolean recycled = false;
- private boolean hasMipMap;
- private boolean requestPremultiplied = true;
- private boolean hasAlpha;
- private ColorSpace colorSpace;
+/** Base class for {@link Bitmap} shadows. */
+@Implements(value = Bitmap.class, shadowPicker = Picker.class)
+public abstract class ShadowBitmap {
/**
* Returns a textual representation of the appearance of the object.
@@ -87,264 +23,13 @@ public class ShadowBitmap {
return shadowBitmap.getDescription();
}
- @Implementation
- protected static Bitmap createBitmap(int width, int height, Bitmap.Config config) {
- return createBitmap((DisplayMetrics) null, width, height, config);
- }
-
- @Implementation(minSdk = JELLY_BEAN_MR1)
- protected static Bitmap createBitmap(
- DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) {
- return createBitmap(displayMetrics, width, height, config, true);
- }
-
- @Implementation(minSdk = JELLY_BEAN_MR1)
- protected static Bitmap createBitmap(
- DisplayMetrics displayMetrics,
- int width,
- int height,
- Bitmap.Config config,
- boolean hasAlpha) {
- if (width <= 0 || height <= 0) {
- throw new IllegalArgumentException("width and height must be > 0");
- }
- checkNotNull(config);
- Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
- ShadowBitmap shadowBitmap = Shadow.extract(scaledBitmap);
- shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")");
-
- shadowBitmap.width = width;
- shadowBitmap.height = height;
- shadowBitmap.config = config;
- shadowBitmap.hasAlpha = hasAlpha;
- shadowBitmap.setMutable(true);
- if (displayMetrics != null) {
- scaledBitmap.setDensity(displayMetrics.densityDpi);
- }
- shadowBitmap.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
- if (RuntimeEnvironment.getApiLevel() >= O) {
- shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
- }
- return scaledBitmap;
- }
-
- @Implementation(minSdk = O)
- protected static Bitmap createBitmap(
- int width, int height, Bitmap.Config config, boolean hasAlpha, ColorSpace colorSpace) {
- checkArgument(colorSpace != null || config == Bitmap.Config.ALPHA_8);
- Bitmap bitmap = createBitmap(null, width, height, config, hasAlpha);
- ShadowBitmap shadowBitmap = Shadows.shadowOf(bitmap);
- shadowBitmap.colorSpace = colorSpace;
- return bitmap;
- }
-
- @Implementation
- protected static Bitmap createBitmap(
- Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) {
- if (x == 0
- && y == 0
- && width == src.getWidth()
- && height == src.getHeight()
- && (matrix == null || matrix.isIdentity())) {
- return src; // Return the original.
- }
-
- if (x + width > src.getWidth()) {
- throw new IllegalArgumentException("x + width must be <= bitmap.width()");
- }
- if (y + height > src.getHeight()) {
- throw new IllegalArgumentException("y + height must be <= bitmap.height()");
- }
-
- Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
- ShadowBitmap shadowNewBitmap = Shadow.extract(newBitmap);
-
- ShadowBitmap shadowSrcBitmap = Shadow.extract(src);
- shadowNewBitmap.appendDescription(shadowSrcBitmap.getDescription());
- shadowNewBitmap.appendDescription(" at (" + x + "," + y + ")");
- shadowNewBitmap.appendDescription(" with width " + width + " and height " + height);
-
- shadowNewBitmap.createdFromBitmap = src;
- shadowNewBitmap.createdFromX = x;
- shadowNewBitmap.createdFromY = y;
- shadowNewBitmap.createdFromWidth = width;
- shadowNewBitmap.createdFromHeight = height;
- shadowNewBitmap.createdFromMatrix = matrix;
- shadowNewBitmap.createdFromFilter = filter;
- shadowNewBitmap.config = src.getConfig();
- if (matrix != null) {
- ShadowMatrix shadowMatrix = Shadow.extract(matrix);
- shadowNewBitmap.appendDescription(" using matrix " + shadowMatrix.getDescription());
-
- // Adjust width and height by using the matrix.
- RectF mappedRect = new RectF();
- matrix.mapRect(mappedRect, new RectF(0, 0, width, height));
- width = Math.round(mappedRect.width());
- height = Math.round(mappedRect.height());
- }
- if (filter) {
- shadowNewBitmap.appendDescription(" with filter");
- }
-
- // updated if matrix is non-null
- shadowNewBitmap.width = width;
- shadowNewBitmap.height = height;
- shadowNewBitmap.setMutable(true);
- newBitmap.setDensity(src.getDensity());
- if ((matrix == null || matrix.isIdentity()) && shadowSrcBitmap.bufferedImage != null) {
- // Only simple cases are supported for setting image data to the new Bitmap.
- shadowNewBitmap.bufferedImage =
- shadowSrcBitmap.bufferedImage.getSubimage(x, y, width, height);
- }
- if (RuntimeEnvironment.getApiLevel() >= O) {
- shadowNewBitmap.colorSpace = shadowSrcBitmap.colorSpace;
- }
- return newBitmap;
- }
-
- @Implementation
- protected static Bitmap createBitmap(
- int[] colors, int offset, int stride, int width, int height, Bitmap.Config config) {
- return createBitmap(null, colors, offset, stride, width, height, config);
- }
-
- @Implementation(minSdk = JELLY_BEAN_MR1)
- protected static Bitmap createBitmap(
- DisplayMetrics displayMetrics,
- int[] colors,
- int offset,
- int stride,
- int width,
- int height,
- Bitmap.Config config) {
- if (width <= 0) {
- throw new IllegalArgumentException("width must be > 0");
- }
- if (height <= 0) {
- throw new IllegalArgumentException("height must be > 0");
- }
- if (Math.abs(stride) < width) {
- throw new IllegalArgumentException("abs(stride) must be >= width");
- }
- checkNotNull(config);
- int lastScanline = offset + (height - 1) * stride;
- int length = colors.length;
- if (offset < 0
- || (offset + width > length)
- || lastScanline < 0
- || (lastScanline + width > length)) {
- throw new ArrayIndexOutOfBoundsException();
- }
-
- BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
- bufferedImage.setRGB(0, 0, width, height, colors, offset, stride);
- Bitmap bitmap = createBitmap(bufferedImage, width, height, config);
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
- shadowBitmap.setMutable(false);
- shadowBitmap.createdFromColors = colors;
- if (displayMetrics != null) {
- bitmap.setDensity(displayMetrics.densityDpi);
- }
- if (RuntimeEnvironment.getApiLevel() >= O) {
- shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
- }
- return bitmap;
- }
-
- private static Bitmap createBitmap(
- BufferedImage bufferedImage, int width, int height, Bitmap.Config config) {
- Bitmap newBitmap = Bitmap.createBitmap(width, height, config);
- ShadowBitmap shadowBitmap = Shadow.extract(newBitmap);
- shadowBitmap.bufferedImage = bufferedImage;
- return newBitmap;
- }
-
- @Implementation
- protected static Bitmap createScaledBitmap(
- Bitmap src, int dstWidth, int dstHeight, boolean filter) {
- if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) {
- return src; // Return the original.
- }
- if (dstWidth <= 0 || dstHeight <= 0) {
- throw new IllegalArgumentException("width and height must be > 0");
- }
- Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
- ShadowBitmap shadowBitmap = Shadow.extract(scaledBitmap);
-
- ShadowBitmap shadowSrcBitmap = Shadow.extract(src);
- shadowBitmap.appendDescription(shadowSrcBitmap.getDescription());
- shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight);
- if (filter) {
- shadowBitmap.appendDescription(" with filter " + filter);
- }
-
- shadowBitmap.createdFromBitmap = src;
- shadowBitmap.scaledFromBitmap = src;
- shadowBitmap.createdFromFilter = filter;
- shadowBitmap.width = dstWidth;
- shadowBitmap.height = dstHeight;
- shadowBitmap.config = src.getConfig();
- shadowBitmap.mutable = true;
- if (!ImageUtil.scaledBitmap(src, scaledBitmap, filter)) {
- shadowBitmap.bufferedImage =
- new BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB);
- shadowBitmap.setPixelsInternal(
- new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()],
- 0,
- 0,
- 0,
- 0,
- shadowBitmap.getWidth(),
- shadowBitmap.getHeight());
- }
- if (RuntimeEnvironment.getApiLevel() >= O) {
- shadowBitmap.colorSpace = shadowSrcBitmap.colorSpace;
- }
- return scaledBitmap;
- }
-
- @Implementation
- protected static Bitmap nativeCreateFromParcel(Parcel p) {
- int parceledWidth = p.readInt();
- int parceledHeight = p.readInt();
- Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();
-
- int[] parceledColors = new int[parceledHeight * parceledWidth];
- p.readIntArray(parceledColors);
-
- return createBitmap(
- parceledColors, 0, parceledWidth, parceledWidth, parceledHeight, parceledConfig);
- }
-
- static int getBytesPerPixel(Bitmap.Config config) {
- if (config == null) {
- throw new NullPointerException("Bitmap config was null.");
- }
- switch (config) {
- case RGBA_F16:
- return 8;
- case ARGB_8888:
- case HARDWARE:
- return 4;
- case RGB_565:
- case ARGB_4444:
- return 2;
- case ALPHA_8:
- return 1;
- default:
- throw new IllegalArgumentException("Unknown bitmap config: " + config);
- }
- }
-
/**
* Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap
* was not copied from another instance.
*
* @return Original Bitmap from which this Bitmap was created.
*/
- public Bitmap getCreatedFromBitmap() {
- return createdFromBitmap;
- }
+ public abstract Bitmap getCreatedFromBitmap();
/**
* Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created from a
@@ -352,9 +37,7 @@ public class ShadowBitmap {
*
* @return Resource ID from which this Bitmap was created.
*/
- public int getCreatedFromResId() {
- return createdFromResId;
- }
+ public abstract int getCreatedFromResId();
/**
* Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a
@@ -362,9 +45,7 @@ public class ShadowBitmap {
*
* @return Path from which this Bitmap was created.
*/
- public String getCreatedFromPath() {
- return createdFromPath;
- }
+ public abstract String getCreatedFromPath();
/**
* {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not
@@ -372,9 +53,7 @@ public class ShadowBitmap {
*
* @return InputStream from which this Bitmap was created.
*/
- public InputStream getCreatedFromStream() {
- return createdFromStream;
- }
+ public abstract InputStream getCreatedFromStream();
/**
* Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from
@@ -382,27 +61,21 @@ public class ShadowBitmap {
*
* @return Bytes from which this Bitmap was created.
*/
- public byte[] getCreatedFromBytes() {
- return createdFromBytes;
- }
+ public abstract byte[] getCreatedFromBytes();
/**
* Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
*
* @return Horizontal offset within {@link #getCreatedFromBitmap()}.
*/
- public int getCreatedFromX() {
- return createdFromX;
- }
+ public abstract int getCreatedFromX();
/**
* Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
*
* @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
*/
- public int getCreatedFromY() {
- return createdFromY;
- }
+ public abstract int getCreatedFromY();
/**
* Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
@@ -411,9 +84,7 @@ public class ShadowBitmap {
* @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
* Bitmap's content, or -1.
*/
- public int getCreatedFromWidth() {
- return createdFromWidth;
- }
+ public abstract int getCreatedFromWidth();
/**
* Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
@@ -422,9 +93,7 @@ public class ShadowBitmap {
* @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
* Bitmap's content, or -1.
*/
- public int getCreatedFromHeight() {
- return createdFromHeight;
- }
+ public abstract int getCreatedFromHeight();
/**
* Color array from which this Bitmap was created. {@code null} if this Bitmap was not created
@@ -432,487 +101,35 @@ public class ShadowBitmap {
*
* @return Color array from which this Bitmap was created.
*/
- public int[] getCreatedFromColors() {
- return createdFromColors;
- }
+ public abstract int[] getCreatedFromColors();
/**
* Matrix from which this Bitmap's content was transformed, or {@code null}.
*
* @return Matrix from which this Bitmap's content was transformed, or {@code null}.
*/
- public Matrix getCreatedFromMatrix() {
- return createdFromMatrix;
- }
+ public abstract Matrix getCreatedFromMatrix();
/**
* {@code true} if this Bitmap was created with filtering.
*
* @return {@code true} if this Bitmap was created with filtering.
*/
- public boolean getCreatedFromFilter() {
- return createdFromFilter;
- }
-
- @Implementation(minSdk = S)
- public Bitmap asShared() {
- setMutable(false);
- return realBitmap;
- }
-
- @Implementation
- protected boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
- appendDescription(" compressed as " + format + " with quality " + quality);
- return ImageUtil.writeToStream(realBitmap, format, quality, stream);
- }
-
- @Implementation
- protected void setPixels(
- int[] pixels, int offset, int stride, int x, int y, int width, int height) {
- checkBitmapMutable();
- setPixelsInternal(pixels, offset, stride, x, y, width, height);
- }
-
- void setPixelsInternal(
- int[] pixels, int offset, int stride, int x, int y, int width, int height) {
- if (bufferedImage == null) {
- bufferedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
- }
- bufferedImage.setRGB(x, y, width, height, pixels, offset, stride);
- }
-
- @Implementation
- protected int getPixel(int x, int y) {
- internalCheckPixelAccess(x, y);
- if (bufferedImage != null) {
- // Note that getPixel() returns a non-premultiplied ARGB value; if
- // config is RGB_565, our return value will likely be more precise than
- // on a physical device, since it needs to map each color component from
- // 5 or 6 bits to 8 bits.
- return bufferedImage.getRGB(x, y);
- } else {
- return 0;
- }
- }
-
- @Implementation
- protected void setPixel(int x, int y, int color) {
- checkBitmapMutable();
- internalCheckPixelAccess(x, y);
- if (bufferedImage == null) {
- bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
- }
- bufferedImage.setRGB(x, y, color);
- }
-
- /**
- * Note that this method will return a RuntimeException unless: - {@code pixels} has the same
- * length as the number of pixels of the bitmap. - {@code x = 0} - {@code y = 0} - {@code width}
- * and {@code height} height match the current bitmap's dimensions.
- */
- @Implementation
- protected void getPixels(
- int[] pixels, int offset, int stride, int x, int y, int width, int height) {
- bufferedImage.getRGB(x, y, width, height, pixels, offset, stride);
- }
-
- @Implementation
- protected int getRowBytes() {
- return getBytesPerPixel(config) * getWidth();
- }
-
- @Implementation
- protected int getByteCount() {
- return getRowBytes() * getHeight();
- }
-
- @Implementation
- protected void recycle() {
- recycled = true;
- }
-
- @Implementation
- protected final boolean isRecycled() {
- return recycled;
- }
-
- @Implementation
- protected Bitmap copy(Bitmap.Config config, boolean isMutable) {
- Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
- ShadowBitmap shadowBitmap = Shadow.extract(newBitmap);
- shadowBitmap.createdFromBitmap = realBitmap;
- shadowBitmap.config = config;
- shadowBitmap.mutable = isMutable;
- shadowBitmap.height = getHeight();
- shadowBitmap.width = getWidth();
- if (bufferedImage != null) {
- ColorModel cm = bufferedImage.getColorModel();
- WritableRaster raster =
- bufferedImage.copyData(bufferedImage.getRaster().createCompatibleWritableRaster());
- shadowBitmap.bufferedImage = new BufferedImage(cm, raster, false, null);
- }
- return newBitmap;
- }
-
- @Implementation(minSdk = KITKAT)
- protected final int getAllocationByteCount() {
- return getRowBytes() * getHeight();
- }
-
- @Implementation
- protected final Bitmap.Config getConfig() {
- return config;
- }
-
- @Implementation(minSdk = KITKAT)
- protected void setConfig(Bitmap.Config config) {
- this.config = config;
- }
-
- @Implementation
- protected final boolean isMutable() {
- return mutable;
- }
-
- public void setMutable(boolean mutable) {
- this.mutable = mutable;
- }
-
- public void appendDescription(String s) {
- description += s;
- }
-
- public String getDescription() {
- return description;
- }
-
- public void setDescription(String s) {
- description = s;
- }
-
- @Implementation
- protected final boolean hasAlpha() {
- return hasAlpha && config != Bitmap.Config.RGB_565;
- }
-
- @Implementation
- protected void setHasAlpha(boolean hasAlpha) {
- this.hasAlpha = hasAlpha;
- }
-
- @Implementation
- protected Bitmap extractAlpha() {
- WritableRaster raster = bufferedImage.getAlphaRaster();
- BufferedImage alphaImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
- alphaImage.getAlphaRaster().setRect(raster);
- return createBitmap(alphaImage, getWidth(), getHeight(), Bitmap.Config.ALPHA_8);
- }
-
- /**
- * This shadow implementation ignores the given paint and offsetXY and simply calls {@link
- * #extractAlpha()}.
- */
- @Implementation
- protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
- return extractAlpha();
- }
-
- @Implementation(minSdk = JELLY_BEAN_MR1)
- protected final boolean hasMipMap() {
- return hasMipMap;
- }
-
- @Implementation(minSdk = JELLY_BEAN_MR1)
- protected final void setHasMipMap(boolean hasMipMap) {
- this.hasMipMap = hasMipMap;
- }
-
- @Implementation
- protected int getWidth() {
- return width;
- }
-
- @Implementation(minSdk = KITKAT)
- protected void setWidth(int width) {
- this.width = width;
- }
-
- @Implementation
- protected int getHeight() {
- return height;
- }
-
- @Implementation(minSdk = KITKAT)
- protected void setHeight(int height) {
- this.height = height;
- }
+ public abstract boolean getCreatedFromFilter();
- @Implementation
- protected int getGenerationId() {
- return 0;
- }
+ public abstract void setMutable(boolean mutable);
- @Implementation(minSdk = M)
- protected Bitmap createAshmemBitmap() {
- return realBitmap;
- }
+ public abstract void appendDescription(String s);
- @Implementation
- protected void eraseColor(int color) {
- if (bufferedImage != null) {
- int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
- Arrays.fill(pixels, color);
- }
- setDescription(String.format("Bitmap (%d, %d)", width, height));
- if (color != 0) {
- appendDescription(String.format(" erased with 0x%08x", color));
- }
- }
-
- @Implementation
- protected void writeToParcel(Parcel p, int flags) {
- p.writeInt(width);
- p.writeInt(height);
- p.writeSerializable(config);
- int[] pixels = new int[width * height];
- getPixels(pixels, 0, width, 0, 0, width, height);
- p.writeIntArray(pixels);
- }
+ public abstract String getDescription();
- @Implementation
- protected void copyPixelsFromBuffer(Buffer dst) {
- if (isRecycled()) {
- throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap");
- }
-
- // See the related comment in #copyPixelsToBuffer(Buffer).
- if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
- throw new RuntimeException(
- "Not implemented: only Bitmaps with "
- + INTERNAL_BYTES_PER_PIXEL
- + " bytes per pixel are supported");
- }
- if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
- throw new RuntimeException("Not implemented: unsupported Buffer subclass");
- }
-
- ByteBuffer byteBuffer = null;
- IntBuffer intBuffer;
- if (dst instanceof IntBuffer) {
- intBuffer = (IntBuffer) dst;
- } else {
- byteBuffer = (ByteBuffer) dst;
- intBuffer = byteBuffer.asIntBuffer();
- }
-
- if (intBuffer.remaining() < (width * height)) {
- throw new RuntimeException("Buffer not large enough for pixels");
- }
-
- int[] colors = new int[width * height];
- intBuffer.get(colors);
- if (byteBuffer != null) {
- byteBuffer.position(byteBuffer.position() + intBuffer.position() * INTERNAL_BYTES_PER_PIXEL);
- }
- int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
- System.arraycopy(colors, 0, pixels, 0, pixels.length);
- }
-
- @Implementation
- protected void copyPixelsToBuffer(Buffer dst) {
- // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels
- // internally. Clients of this API probably expect that the buffer size must be >=
- // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other
- // configs that value would be smaller then the buffer size we actually need.
- if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
- throw new RuntimeException(
- "Not implemented: only Bitmaps with "
- + INTERNAL_BYTES_PER_PIXEL
- + " bytes per pixel are supported");
- }
+ public abstract void setDescription(String s);
- if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
- throw new RuntimeException("Not implemented: unsupported Buffer subclass");
+ /** A {@link ShadowPicker} that always selects the legacy ShadowBitmap. */
+ public static class Picker implements ShadowPicker<ShadowBitmap> {
+ @Override
+ public Class<? extends ShadowBitmap> pickShadowClass() {
+ return ShadowLegacyBitmap.class;
}
- int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
- if (dst instanceof ByteBuffer) {
- IntBuffer intBuffer = ((ByteBuffer) dst).asIntBuffer();
- intBuffer.put(pixels);
- dst.position(intBuffer.position() * 4);
- } else if (dst instanceof IntBuffer) {
- ((IntBuffer) dst).put(pixels);
- }
- }
-
- @Implementation(minSdk = KITKAT)
- protected void reconfigure(int width, int height, Bitmap.Config config) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.config == Bitmap.Config.HARDWARE) {
- throw new IllegalStateException("native-backed bitmaps may not be reconfigured");
- }
-
- // This should throw if the resulting allocation size is greater than the initial allocation
- // size of our Bitmap, but we don't keep track of that information reliably, so we're forced to
- // assume that our original dimensions and config are large enough to fit the new dimensions and
- // config
- this.width = width;
- this.height = height;
- this.config = config;
- bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
- }
-
- @Implementation(minSdk = KITKAT)
- protected boolean isPremultiplied() {
- return requestPremultiplied && hasAlpha();
- }
-
- @Implementation(minSdk = KITKAT)
- protected void setPremultiplied(boolean isPremultiplied) {
- this.requestPremultiplied = isPremultiplied;
- }
-
- @Implementation(minSdk = O)
- protected ColorSpace getColorSpace() {
- return colorSpace;
- }
-
- @Implementation(minSdk = Q)
- protected void setColorSpace(ColorSpace colorSpace) {
- this.colorSpace = checkNotNull(colorSpace);
- }
-
- @Implementation
- protected boolean sameAs(Bitmap other) {
- if (other == null) {
- return false;
- }
- ShadowBitmap shadowOtherBitmap = Shadow.extract(other);
- if (this.width != shadowOtherBitmap.width || this.height != shadowOtherBitmap.height) {
- return false;
- }
- if (this.config != shadowOtherBitmap.config) {
- return false;
- }
-
- if (bufferedImage == null && shadowOtherBitmap.bufferedImage != null) {
- return false;
- } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage == null) {
- return false;
- } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage != null) {
- int[] pixels = ((DataBufferInt) bufferedImage.getData().getDataBuffer()).getData();
- int[] otherPixels =
- ((DataBufferInt) shadowOtherBitmap.bufferedImage.getData().getDataBuffer()).getData();
- if (!Arrays.equals(pixels, otherPixels)) {
- return false;
- }
- }
- // When Bitmap.createScaledBitmap is called, the colors array is cleared, so we need a basic
- // way to detect if two scaled bitmaps are the same.
- if (scaledFromBitmap != null && shadowOtherBitmap.scaledFromBitmap != null) {
- return scaledFromBitmap.sameAs(shadowOtherBitmap.scaledFromBitmap);
- }
- return true;
- }
-
- public void setCreatedFromResId(int resId, String description) {
- this.createdFromResId = resId;
- appendDescription(" for resource:" + description);
- }
-
- private void checkBitmapMutable() {
- if (isRecycled()) {
- throw new IllegalStateException("Can't call setPixel() on a recycled bitmap");
- } else if (!isMutable()) {
- throw new IllegalStateException("Bitmap is immutable");
- }
- }
-
- private void internalCheckPixelAccess(int x, int y) {
- if (x < 0) {
- throw new IllegalArgumentException("x must be >= 0");
- }
- if (y < 0) {
- throw new IllegalArgumentException("y must be >= 0");
- }
- if (x >= getWidth()) {
- throw new IllegalArgumentException("x must be < bitmap.width()");
- }
- if (y >= getHeight()) {
- throw new IllegalArgumentException("y must be < bitmap.height()");
- }
- }
-
- void drawRect(Rect r, Paint paint) {
- if (bufferedImage == null) {
- return;
- }
- int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
-
- Rect toDraw =
- new Rect(
- max(0, r.left), max(0, r.top), min(getWidth(), r.right), min(getHeight(), r.bottom));
- if (toDraw.left == 0 && toDraw.top == 0 && toDraw.right == getWidth()) {
- Arrays.fill(pixels, 0, getWidth() * toDraw.bottom, paint.getColor());
- return;
- }
- for (int y = toDraw.top; y < toDraw.bottom; y++) {
- Arrays.fill(
- pixels, y * getWidth() + toDraw.left, y * getWidth() + toDraw.right, paint.getColor());
- }
- }
-
- void drawRect(RectF r, Paint paint) {
- if (bufferedImage == null) {
- return;
- }
-
- Graphics2D graphics2D = bufferedImage.createGraphics();
- Rectangle2D r2d = new Rectangle2D.Float(r.left, r.top, r.right - r.left, r.bottom - r.top);
- graphics2D.setColor(new Color(paint.getColor()));
- graphics2D.draw(r2d);
- graphics2D.dispose();
- }
-
- void drawBitmap(Bitmap source, int left, int top) {
- ShadowBitmap shadowSource = Shadows.shadowOf(source);
- if (bufferedImage == null || shadowSource.bufferedImage == null) {
- // pixel data not available, so there's nothing we can do
- return;
- }
-
- int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
- int[] sourcePixels =
- ((DataBufferInt) shadowSource.bufferedImage.getRaster().getDataBuffer()).getData();
-
- // fast path
- if (left == 0 && top == 0 && getWidth() == source.getWidth()) {
- int size = min(getWidth() * getHeight(), source.getWidth() * source.getHeight());
- System.arraycopy(sourcePixels, 0, pixels, 0, size);
- return;
- }
- // slower (row-by-row) path
- int startSourceY = max(0, -top);
- int startSourceX = max(0, -left);
- int startY = max(0, top);
- int startX = max(0, left);
- int endY = min(getHeight(), top + source.getHeight());
- int endX = min(getWidth(), left + source.getWidth());
- int lenY = endY - startY;
- int lenX = endX - startX;
- for (int y = 0; y < lenY; y++) {
- System.arraycopy(
- sourcePixels,
- (startSourceY + y) * source.getWidth() + startSourceX,
- pixels,
- (startY + y) * getWidth() + startX,
- lenX);
- }
- }
-
- BufferedImage getBufferedImage() {
- return bufferedImage;
- }
-
- void setBufferedImage(BufferedImage bufferedImage) {
- this.bufferedImage = bufferedImage;
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java
index de499e9e0..3bdf7abf6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapDrawable.java
@@ -42,8 +42,8 @@ public class ShadowBitmapDrawable extends ShadowDrawable {
protected void setCreatedFromResId(int createdFromResId, String resourceName) {
super.setCreatedFromResId(createdFromResId, resourceName);
Bitmap bitmap = realBitmapDrawable.getBitmap();
- if (bitmap != null && Shadow.extract(bitmap) instanceof ShadowBitmap) {
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ if (bitmap != null && Shadow.extract(bitmap) instanceof ShadowLegacyBitmap) {
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
if (shadowBitmap.createdFromResId == -1) {
shadowBitmap.setCreatedFromResId(createdFromResId, resourceName);
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
index 33bc8fd4b..fd2c084a5 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBitmapFactory.java
@@ -85,7 +85,7 @@ public class ShadowBitmapFactory {
return null;
}
Bitmap bitmap = create("resource:" + resourceName, options, image);
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.createdFromResId = id;
return bitmap;
}
@@ -116,7 +116,7 @@ public class ShadowBitmapFactory {
return null;
}
Bitmap bitmap = create("file:" + pathName, options, image);
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.createdFromPath = pathName;
return bitmap;
}
@@ -143,7 +143,7 @@ public class ShadowBitmapFactory {
return null;
}
Bitmap bitmap = create("fd:" + fd, null, outPadding, opts, null, image);
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.createdFromFileDescriptor = fd;
return bitmap;
}
@@ -189,7 +189,7 @@ public class ShadowBitmapFactory {
Bitmap bitmap = create(name, null, outPadding, opts, null, image);
ReflectionHelpers.callInstanceMethod(
bitmap, "setNinePatchChunk", ClassParameter.from(byte[].class, ninePatchChunk));
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.createdFromStream = is;
if (image != null && opts != null) {
@@ -222,7 +222,7 @@ public class ShadowBitmapFactory {
return null;
}
Bitmap bitmap = create(desc, data, null, opts, null, image);
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.createdFromBytes = data;
return bitmap;
}
@@ -242,7 +242,7 @@ public class ShadowBitmapFactory {
final Point widthAndHeightOverride,
final RobolectricBufferedImage image) {
Bitmap bitmap = Shadow.newInstanceOf(Bitmap.class);
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
shadowBitmap.appendDescription(name == null ? "Bitmap" : "Bitmap for " + name);
Bitmap.Config config;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
index 863458509..f8725068e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothAdapter.java
@@ -30,11 +30,14 @@ import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.ParcelUuid;
import android.provider.Settings;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -99,6 +102,8 @@ public class ShadowBluetoothAdapter {
private final Map<Integer, BluetoothProfile> profileProxies = new HashMap<>();
private final ConcurrentMap<UUID, BackgroundRfcommServerEntry> backgroundRfcommServers =
new ConcurrentHashMap<>();
+ private final Map<Integer, List<BluetoothProfile.ServiceListener>>
+ bluetoothProfileServiceListeners = new HashMap<>();
@Resetter
public static void reset() {
@@ -528,6 +533,13 @@ public class ShadowBluetoothAdapter {
return false;
} else {
listener.onServiceConnected(profile, proxy);
+ List<BluetoothProfile.ServiceListener> profileListeners =
+ bluetoothProfileServiceListeners.get(profile);
+ if (profileListeners != null) {
+ profileListeners.add(listener);
+ } else {
+ bluetoothProfileServiceListeners.put(profile, new ArrayList<>(ImmutableList.of(listener)));
+ }
return true;
}
}
@@ -548,6 +560,13 @@ public class ShadowBluetoothAdapter {
if (proxy != null && proxy.equals(profileProxies.get(profile))) {
profileProxies.remove(profile);
+ List<BluetoothProfile.ServiceListener> profileListeners =
+ bluetoothProfileServiceListeners.remove(profile);
+ if (profileListeners != null) {
+ for (BluetoothProfile.ServiceListener listener : profileListeners) {
+ listener.onServiceDisconnected(profile);
+ }
+ }
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java
index 95c75476e..d7ed1cad0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothDevice.java
@@ -7,6 +7,7 @@ import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.annotation.IntRange;
@@ -16,10 +17,10 @@ import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothSocket;
+import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.IBluetooth;
import android.content.Context;
import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
import android.os.Handler;
import android.os.ParcelUuid;
import java.io.IOException;
@@ -39,7 +40,8 @@ import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;
-@Implements(BluetoothDevice.class)
+/** Shadow for {@link BluetoothDevice}. */
+@Implements(value = BluetoothDevice.class, looseSignatures = true)
public class ShadowBluetoothDevice {
@Deprecated // Prefer {@link android.bluetooth.BluetoothAdapter#getRemoteDevice}
public static BluetoothDevice newInstance(String address) {
@@ -103,8 +105,14 @@ public class ShadowBluetoothDevice {
*
* @param alias alias name.
*/
- public void setAlias(String alias) {
- this.alias = alias;
+ @Implementation
+ public Object setAlias(Object alias) {
+ this.alias = (String) alias;
+ if (RuntimeEnvironment.getApiLevel() >= S) {
+ return BluetoothStatusCodes.SUCCESS;
+ } else {
+ return true;
+ }
}
/**
@@ -438,7 +446,7 @@ public class ShadowBluetoothDevice {
private void checkForBluetoothConnectPermission() {
if (shouldThrowSecurityExceptions
- && VERSION.SDK_INT >= VERSION_CODES.S
+ && VERSION.SDK_INT >= S
&& !checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT)) {
throw new SecurityException("Bluetooth connect permission required.");
}
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 5f5fd8807..737df1fb8 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
@@ -10,15 +10,41 @@ import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
import android.content.Context;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.annotation.ReflectorObject;
import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.PerfStatsCollector;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+/** Shadow implementation of {@link BluetoothGatt}. */
@Implements(value = BluetoothGatt.class, minSdk = JELLY_BEAN_MR2)
public class ShadowBluetoothGatt {
+
+ private static final String NULL_CALLBACK_MSG = "BluetoothGattCallback can not be null.";
+
private BluetoothGattCallback bluetoothGattCallback;
+ private int connectionPriority = BluetoothGatt.CONNECTION_PRIORITY_BALANCED;
+ private boolean isConnected = false;
+ private boolean isClosed = false;
+ private byte[] writtenBytes;
+ private byte[] readBytes;
+ private final Set<BluetoothGattService> discoverableServices = new HashSet<>();
+ private final ArrayList<BluetoothGattService> services = new ArrayList<>();
+
+ @RealObject private BluetoothGatt realBluetoothGatt;
+ @ReflectorObject protected BluetoothGattReflector bluetoothGattReflector;
@SuppressLint("PrivateApi")
@SuppressWarnings("unchecked")
@@ -77,27 +103,204 @@ public class ShadowBluetoothGatt {
new Class<?>[] {Context.class, iBluetoothGattClass, BluetoothDevice.class},
new Object[] {RuntimeEnvironment.getApplication(), null, device});
}
+
+ PerfStatsCollector.getInstance().incrementCount("constructShadowBluetoothGatt");
return bluetoothGatt;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
- /* package */ BluetoothGattCallback getGattCallback() {
- return bluetoothGattCallback;
+ /**
+ * Connect to a remote device, and performs a {@link
+ * BluetoothGattCallback#onConnectionStateChange} if a {@link BluetoothGattCallback} has been set
+ * by {@link ShadowBluetoothGatt#setGattCallback}
+ *
+ * @return true, if a {@link BluetoothGattCallback} has been set by {@link
+ * ShadowBluetoothGatt#setGattCallback}
+ */
+ @Implementation(minSdk = JELLY_BEAN_MR2)
+ protected boolean connect() {
+ if (this.getGattCallback() != null) {
+ this.isConnected = true;
+ this.getGattCallback()
+ .onConnectionStateChange(
+ this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);
+ return true;
+ }
+ return false;
}
- /* package */ void setGattCallback(BluetoothGattCallback bluetoothGattCallback) {
- this.bluetoothGattCallback = bluetoothGattCallback;
+ /**
+ * Disconnects an established connection, or cancels a connection attempt currently in progress.
+ */
+ @Implementation(minSdk = JELLY_BEAN_MR2)
+ protected void disconnect() {
+ bluetoothGattReflector.disconnect();
+ if (this.getGattCallback() != null && this.isConnected) {
+ this.getGattCallback()
+ .onConnectionStateChange(
+ this.realBluetoothGatt,
+ BluetoothGatt.GATT_SUCCESS,
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+ this.isConnected = false;
+ }
+
+ /** Close this Bluetooth GATT client. */
+ @Implementation(minSdk = JELLY_BEAN_MR2)
+ protected void close() {
+ bluetoothGattReflector.close();
+ this.isClosed = true;
+ this.isConnected = false;
}
/**
- * Overrides behavior of {@link BluetoothGatt#connect()} to always return true.
+ * Request a connection parameter update.
*
- * @return true, unconditionally
+ * @param priority Request a specific connection priority. Must be one of {@link
+ * BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}
+ * or {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}.
+ * @return true if operation is successful.
+ * @throws IllegalArgumentException If the parameters are outside of their specified range.
*/
- @Implementation(minSdk = JELLY_BEAN_MR2)
- protected boolean connect() {
+ @Implementation(minSdk = O)
+ protected boolean requestConnectionPriority(int priority) {
+ if (priority == BluetoothGatt.CONNECTION_PRIORITY_HIGH
+ || priority == BluetoothGatt.CONNECTION_PRIORITY_BALANCED
+ || priority == BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
+ this.connectionPriority = priority;
+ return true;
+ }
+ throw new IllegalArgumentException("connection priority not within valid range");
+ }
+
+ /**
+ * Overrides {@link BluetoothGatt#discoverServices} to always return false unless there are
+ * discoverable services made available by {@link ShadowBluetoothGatt#addDiscoverableService}
+ *
+ * @return true if discoverable service is available and callback response is possible
+ */
+ @Implementation(minSdk = O)
+ protected boolean discoverServices() {
+ this.services.clear();
+ if (!this.discoverableServices.isEmpty()) {
+ this.services.addAll(this.discoverableServices);
+
+ if (this.getGattCallback() != null) {
+ this.getGattCallback()
+ .onServicesDiscovered(this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Overrides {@link BluetoothGatt#getServices} to always return a list of services discovered.
+ *
+ * @return list of services that have been discovered through {@link
+ * ShadowBluetoothGatt#discoverServices}, empty if none.
+ */
+ @Implementation(minSdk = O)
+ protected List<BluetoothGattService> getServices() {
+ return new ArrayList<>(this.services);
+ }
+
+ /**
+ * Reads bytes from incoming characteristic if properties are valid and callback is set. Callback
+ * responds with {@link BluetoothGattCallback#onCharacteristicWrite} and returns true when
+ * successful.
+ *
+ * @param characteristic Characteristic to read
+ * @return true, if the read operation was initiated successfully
+ * @throws IllegalStateException if a {@link BluetoothGattCallback} has not been set by {@link
+ * ShadowBluetoothGatt#setGattCallback}
+ */
+ public boolean writeIncomingCharacteristic(BluetoothGattCharacteristic characteristic) {
+ if (this.getGattCallback() == null) {
+ throw new IllegalStateException(NULL_CALLBACK_MSG);
+ }
+ if (characteristic.getService() == null
+ || ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0
+ && (characteristic.getProperties()
+ & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)
+ == 0)) {
+ return false;
+ }
+ this.writtenBytes = characteristic.getValue();
+ this.bluetoothGattCallback.onCharacteristicWrite(
+ this.realBluetoothGatt, characteristic, BluetoothGatt.GATT_SUCCESS);
+ return true;
+ }
+
+ /**
+ * Writes bytes from incoming characteristic if properties are valid and callback is set. Callback
+ * responds with BluetoothGattCallback#onCharacteristicRead and returns true when successful.
+ *
+ * @param characteristic Characteristic to read
+ * @return true, if the read operation was initiated successfully
+ * @throws IllegalStateException if a {@link BluetoothGattCallback} has not been set by {@link
+ * ShadowBluetoothGatt#setGattCallback}
+ */
+ public boolean readIncomingCharacteristic(BluetoothGattCharacteristic characteristic) {
+ if (this.getGattCallback() == null) {
+ throw new IllegalStateException(NULL_CALLBACK_MSG);
+ }
+ if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0
+ || characteristic.getService() == null) {
+ return false;
+ }
+
+ this.readBytes = characteristic.getValue();
+ this.bluetoothGattCallback.onCharacteristicRead(
+ this.realBluetoothGatt, characteristic, BluetoothGatt.GATT_SUCCESS);
return true;
}
+
+ public void addDiscoverableService(BluetoothGattService service) {
+ this.discoverableServices.add(service);
+ }
+
+ public void removeDiscoverableService(BluetoothGattService service) {
+ this.discoverableServices.remove(service);
+ }
+
+ public BluetoothGattCallback getGattCallback() {
+ return this.bluetoothGattCallback;
+ }
+
+ public void setGattCallback(BluetoothGattCallback bluetoothGattCallback) {
+ this.bluetoothGattCallback = bluetoothGattCallback;
+ }
+
+ public boolean isConnected() {
+ return this.isConnected;
+ }
+
+ public boolean isClosed() {
+ return this.isClosed;
+ }
+
+ public int getConnectionPriority() {
+ return this.connectionPriority;
+ }
+
+ public byte[] getLatestWrittenBytes() {
+ return this.writtenBytes;
+ }
+
+ public byte[] getLatestReadBytes() {
+ return this.readBytes;
+ }
+
+ @ForType(BluetoothGatt.class)
+ private interface BluetoothGattReflector {
+
+ @Direct
+ void disconnect();
+
+ @Direct
+ void close();
+ }
}
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 51dde02aa..54b96265b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
@@ -2,6 +2,7 @@ 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 android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
@@ -23,6 +24,7 @@ public class ShadowBluetoothHeadset {
private final List<BluetoothDevice> connectedDevices = new ArrayList<>();
private boolean allowsSendVendorSpecificResultCode = true;
private BluetoothDevice activeBluetoothDevice;
+ private boolean isVoiceRecognitionSupported = true;
/**
* Overrides behavior of {@link getConnectedDevices}. Returns list of devices that is set up by
@@ -130,6 +132,27 @@ public class ShadowBluetoothHeadset {
}
/**
+ * Sets whether the headset supports voice recognition.
+ *
+ * <p>By default voice recognition is supported.
+ *
+ * @see #isVoiceRecognitionSupported(BluetoothDevice)
+ */
+ public void setVoiceRecognitionSupported(boolean supported) {
+ isVoiceRecognitionSupported = supported;
+ }
+
+ /**
+ * Checks whether the headset supports voice recognition.
+ *
+ * @see #setVoiceRecognitionSupported(boolean)
+ */
+ @Implementation(minSdk = S)
+ protected boolean isVoiceRecognitionSupported(BluetoothDevice device) {
+ return isVoiceRecognitionSupported;
+ }
+
+ /**
* Affects the behavior of {@link BluetoothHeadset#sendVendorSpecificResultCode}
*
* @param allowsSendVendorSpecificResultCode can be set to 'false' to simulate the situation where
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamcorderProfile.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamcorderProfile.java
new file mode 100644
index 000000000..8576f70a3
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCamcorderProfile.java
@@ -0,0 +1,66 @@
+package org.robolectric.shadows;
+
+import android.media.CamcorderProfile;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Shadow of the CamcorderProfile that allows the caller to add custom profile settings. */
+@Implements(CamcorderProfile.class)
+public class ShadowCamcorderProfile {
+
+ private static final Table<Integer, Integer, CamcorderProfile> profiles = HashBasedTable.create();
+
+ public static void addProfile(int cameraId, int quality, CamcorderProfile profile) {
+ profiles.put(cameraId, quality, profile);
+ }
+
+ @Resetter
+ public static void reset() {
+ profiles.clear();
+ }
+
+ public static CamcorderProfile createProfile(
+ int duration,
+ int quality,
+ int fileFormat,
+ int videoCodec,
+ int videoBitRate,
+ int videoFrameRate,
+ int videoWidth,
+ int videoHeight,
+ int audioCodec,
+ int audioBitRate,
+ int audioSampleRate,
+ int audioChannels) {
+ // CamcorderProfile doesn't have a public constructor. To construct we need to use reflection.
+ return ReflectionHelpers.callConstructor(
+ CamcorderProfile.class,
+ ClassParameter.from(int.class, duration),
+ ClassParameter.from(int.class, quality),
+ ClassParameter.from(int.class, fileFormat),
+ ClassParameter.from(int.class, videoCodec),
+ ClassParameter.from(int.class, videoBitRate),
+ ClassParameter.from(int.class, videoFrameRate),
+ ClassParameter.from(int.class, videoWidth),
+ ClassParameter.from(int.class, videoHeight),
+ ClassParameter.from(int.class, audioCodec),
+ ClassParameter.from(int.class, audioBitRate),
+ ClassParameter.from(int.class, audioSampleRate),
+ ClassParameter.from(int.class, audioChannels));
+ }
+
+ @Implementation
+ protected static boolean hasProfile(int cameraId, int quality) {
+ return profiles.contains(cameraId, quality);
+ }
+
+ @Implementation
+ protected static CamcorderProfile get(int cameraId, int quality) {
+ return profiles.get(cameraId, quality);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java
index 310b610c9..8c962c451 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCanvas.java
@@ -1,582 +1,81 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-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_MR1;
-import static android.os.Build.VERSION_CODES.O;
-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 android.graphics.Bitmap;
import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
-import android.graphics.Rect;
import android.graphics.RectF;
-import com.google.common.base.Preconditions;
-import java.util.ArrayList;
-import java.util.List;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.Shadows;
-import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.annotation.ReflectorObject;
-import org.robolectric.annotation.Resetter;
-import org.robolectric.res.android.NativeObjRegistry;
import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.reflector.Direct;
-import org.robolectric.util.reflector.ForType;
-
-/**
- * Broken. This implementation is very specific to the application for which it was developed. Todo:
- * Reimplement. Consider using the same strategy of collecting a history of draw events and
- * providing methods for writing queries based on type, number, and order of events.
- */
-@SuppressWarnings({"UnusedDeclaration"})
-@Implements(Canvas.class)
-public class ShadowCanvas {
- private static final NativeObjRegistry<NativeCanvas> nativeObjectRegistry =
- new NativeObjRegistry<>(NativeCanvas.class);
-
- @RealObject protected Canvas realCanvas;
- @ReflectorObject protected CanvasReflector canvasReflector;
-
- private final List<RoundRectPaintHistoryEvent> roundRectPaintEvents = new ArrayList<>();
- private List<PathPaintHistoryEvent> pathPaintEvents = new ArrayList<>();
- private List<CirclePaintHistoryEvent> circlePaintEvents = new ArrayList<>();
- private List<ArcPaintHistoryEvent> arcPaintEvents = new ArrayList<>();
- private List<RectPaintHistoryEvent> rectPaintEvents = new ArrayList<>();
- private List<LinePaintHistoryEvent> linePaintEvents = new ArrayList<>();
- private List<OvalPaintHistoryEvent> ovalPaintEvents = new ArrayList<>();
- private List<TextHistoryEvent> drawnTextEventHistory = new ArrayList<>();
- private Paint drawnPaint;
- private Bitmap targetBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
- private float translateX;
- private float translateY;
- private float scaleX = 1;
- private float scaleY = 1;
- private int height;
- private int width;
-
- /**
- * Returns a textual representation of the appearance of the object.
- *
- * @param canvas the canvas to visualize
- * @return The textual representation of the appearance of the object.
- */
- public static String visualize(Canvas canvas) {
- ShadowCanvas shadowCanvas = Shadow.extract(canvas);
- return shadowCanvas.getDescription();
- }
-
- @Implementation
- protected void __constructor__(Bitmap bitmap) {
- canvasReflector.__constructor__(bitmap);
- this.targetBitmap = bitmap;
- }
-
- private long getNativeId() {
- return RuntimeEnvironment.getApiLevel() <= KITKAT_WATCH
- ? (int) ReflectionHelpers.getField(realCanvas, "mNativeCanvas")
- : realCanvas.getNativeCanvasWrapper();
- }
-
- private NativeCanvas getNativeCanvas() {
- return nativeObjectRegistry.getNativeObject(getNativeId());
- }
-
- public void appendDescription(String s) {
- ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
- shadowBitmap.appendDescription(s);
- }
-
- public String getDescription() {
- ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
- return shadowBitmap.getDescription();
- }
-
- @Implementation
- protected void setBitmap(Bitmap bitmap) {
- targetBitmap = bitmap;
- }
-
- @Implementation
- protected void drawText(String text, float x, float y, Paint paint) {
- drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text));
- }
-
- @Implementation
- protected void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) {
- drawnTextEventHistory.add(
- new TextHistoryEvent(x, y, paint, text.subSequence(start, end).toString()));
- }
-
- @Implementation
- protected void drawText(char[] text, int index, int count, float x, float y, Paint paint) {
- drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, new String(text, index, count)));
- }
-
- @Implementation
- protected void drawText(String text, int start, int end, float x, float y, Paint paint) {
- drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text.substring(start, end)));
- }
-
- @Implementation
- protected void translate(float x, float y) {
- this.translateX = x;
- this.translateY = y;
- }
-
- @Implementation
- protected void scale(float sx, float sy) {
- this.scaleX = sx;
- this.scaleY = sy;
- }
-
- @Implementation
- protected void scale(float sx, float sy, float px, float py) {
- this.scaleX = sx;
- this.scaleY = sy;
- }
-
- @Implementation
- protected void drawPaint(Paint paint) {
- drawnPaint = paint;
- }
-
- @Implementation
- protected void drawColor(int color) {
- appendDescription("draw color " + color);
- }
-
- @Implementation
- protected void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) {
- describeBitmap(bitmap, paint);
-
- int x = (int) (left + translateX);
- int y = (int) (top + translateY);
- if (x != 0 || y != 0) {
- appendDescription(" at (" + x + "," + y + ")");
- }
-
- if (scaleX != 1 && scaleY != 1) {
- appendDescription(" scaled by (" + scaleX + "," + scaleY + ")");
- }
-
- if (bitmap != null && targetBitmap != null) {
- ShadowBitmap shadowTargetBitmap = Shadows.shadowOf(targetBitmap);
- shadowTargetBitmap.drawBitmap(bitmap, (int) left, (int) top);
- }
- }
-
- @Implementation
- protected void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) {
- describeBitmap(bitmap, paint);
-
- StringBuilder descriptionBuilder = new StringBuilder();
- if (dst != null) {
- descriptionBuilder
- .append(" at (")
- .append(dst.left)
- .append(",")
- .append(dst.top)
- .append(") with height=")
- .append(dst.height())
- .append(" and width=")
- .append(dst.width());
- }
-
- if (src != null) {
- descriptionBuilder.append(" taken from ").append(src.toString());
- }
- appendDescription(descriptionBuilder.toString());
- }
-
- @Implementation
- protected void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) {
- describeBitmap(bitmap, paint);
-
- StringBuilder descriptionBuilder = new StringBuilder();
- if (dst != null) {
- descriptionBuilder
- .append(" at (")
- .append(dst.left)
- .append(",")
- .append(dst.top)
- .append(") with height=")
- .append(dst.height())
- .append(" and width=")
- .append(dst.width());
- }
-
- if (src != null) {
- descriptionBuilder.append(" taken from ").append(src.toString());
- }
- appendDescription(descriptionBuilder.toString());
- }
-
- @Implementation
- protected void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) {
- describeBitmap(bitmap, paint);
-
- ShadowMatrix shadowMatrix = Shadow.extract(matrix);
- appendDescription(" transformed by " + shadowMatrix.getDescription());
- }
-
- @Implementation
- protected void drawPath(Path path, Paint paint) {
- pathPaintEvents.add(new PathPaintHistoryEvent(new Path(path), new Paint(paint)));
-
- separateLines();
- ShadowPath shadowPath = Shadow.extract(path);
- appendDescription("Path " + shadowPath.getPoints().toString());
- }
-
- @Implementation
- protected void drawCircle(float cx, float cy, float radius, Paint paint) {
- circlePaintEvents.add(new CirclePaintHistoryEvent(cx, cy, radius, paint));
- }
-
- @Implementation
- protected void drawArc(
- RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) {
- arcPaintEvents.add(new ArcPaintHistoryEvent(oval, startAngle, sweepAngle, useCenter, paint));
- }
-
- @Implementation
- protected void drawRect(float left, float top, float right, float bottom, Paint paint) {
- rectPaintEvents.add(new RectPaintHistoryEvent(left, top, right, bottom, paint));
-
- if (targetBitmap != null) {
- ShadowBitmap shadowTargetBitmap = Shadows.shadowOf(targetBitmap);
- shadowTargetBitmap.drawRect(new RectF(left, top, right, bottom), paint);
- }
- }
-
- @Implementation
- protected void drawRect(Rect r, Paint paint) {
- rectPaintEvents.add(new RectPaintHistoryEvent(r.left, r.top, r.right, r.bottom, paint));
-
- if (targetBitmap != null) {
- ShadowBitmap shadowTargetBitmap = Shadows.shadowOf(targetBitmap);
- shadowTargetBitmap.drawRect(r, paint);
- }
- }
-
- @Implementation
- protected void drawRoundRect(RectF rect, float rx, float ry, Paint paint) {
- roundRectPaintEvents.add(
- new RoundRectPaintHistoryEvent(
- rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint));
- }
-
- @Implementation
- protected void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
- linePaintEvents.add(new LinePaintHistoryEvent(startX, startY, stopX, stopY, paint));
- }
-
- @Implementation
- protected void drawOval(RectF oval, Paint paint) {
- ovalPaintEvents.add(new OvalPaintHistoryEvent(oval, paint));
- }
-
- private void describeBitmap(Bitmap bitmap, Paint paint) {
- separateLines();
-
- ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
- appendDescription(shadowBitmap.getDescription());
-
- if (paint != null) {
- ColorFilter colorFilter = paint.getColorFilter();
- if (colorFilter != null) {
- appendDescription(" with " + colorFilter.getClass().getSimpleName());
- }
- }
- }
-
- private void separateLines() {
- if (getDescription().length() != 0) {
- appendDescription("\n");
- }
- }
+import org.robolectric.shadow.api.ShadowPicker;
+import org.robolectric.shadows.ShadowCanvas.CanvasPicker;
- public int getPathPaintHistoryCount() {
- return pathPaintEvents.size();
- }
+/** Base class for {@link Canvas} shadow classes. Mainly contains public shadow API signatures. */
+@Implements(value = Canvas.class, shadowPicker = CanvasPicker.class)
+public abstract class ShadowCanvas {
- public int getCirclePaintHistoryCount() {
- return circlePaintEvents.size();
- }
-
- public int getArcPaintHistoryCount() {
- return arcPaintEvents.size();
- }
-
- public boolean hasDrawnPath() {
- return getPathPaintHistoryCount() > 0;
- }
-
- public boolean hasDrawnCircle() {
- return circlePaintEvents.size() > 0;
- }
-
- public Paint getDrawnPathPaint(int i) {
- return pathPaintEvents.get(i).pathPaint;
- }
-
- public Path getDrawnPath(int i) {
- return pathPaintEvents.get(i).drawnPath;
- }
-
- public CirclePaintHistoryEvent getDrawnCircle(int i) {
- return circlePaintEvents.get(i);
- }
-
- public ArcPaintHistoryEvent getDrawnArc(int i) {
- return arcPaintEvents.get(i);
- }
-
- public void resetCanvasHistory() {
- drawnTextEventHistory.clear();
- pathPaintEvents.clear();
- circlePaintEvents.clear();
- rectPaintEvents.clear();
- roundRectPaintEvents.clear();
- linePaintEvents.clear();
- ovalPaintEvents.clear();
- ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
- shadowBitmap.setDescription("");
- }
-
- public Paint getDrawnPaint() {
- return drawnPaint;
- }
-
- public void setHeight(int height) {
- this.height = height;
- }
-
- public void setWidth(int width) {
- this.width = width;
- }
-
- @Implementation
- protected int getWidth() {
- if (width == 0) {
- return targetBitmap.getWidth();
+ public static String visualize(Canvas canvas) {
+ if (Shadow.extract(canvas) instanceof ShadowLegacyCanvas) {
+ ShadowCanvas shadowCanvas = Shadow.extract(canvas);
+ return shadowCanvas.getDescription();
+ } else {
+ throw new UnsupportedOperationException(
+ "ShadowCanvas.visualize is only supported in legacy Canvas");
}
- return width;
}
- @Implementation
- protected int getHeight() {
- if (height == 0) {
- return targetBitmap.getHeight();
- }
- return height;
- }
+ public abstract void appendDescription(String s);
- @Implementation
- protected boolean getClipBounds(Rect bounds) {
- Preconditions.checkNotNull(bounds);
- if (targetBitmap == null) {
- return false;
- }
- bounds.set(0, 0, targetBitmap.getWidth(), targetBitmap.getHeight());
- return !bounds.isEmpty();
- }
+ public abstract String getDescription();
- public TextHistoryEvent getDrawnTextEvent(int i) {
- return drawnTextEventHistory.get(i);
- }
+ public abstract int getPathPaintHistoryCount();
- public int getTextHistoryCount() {
- return drawnTextEventHistory.size();
- }
+ public abstract int getCirclePaintHistoryCount();
- public RectPaintHistoryEvent getDrawnRect(int i) {
- return rectPaintEvents.get(i);
- }
+ public abstract int getArcPaintHistoryCount();
- public RectPaintHistoryEvent getLastDrawnRect() {
- return rectPaintEvents.get(rectPaintEvents.size() - 1);
- }
+ public abstract boolean hasDrawnPath();
- public int getRectPaintHistoryCount() {
- return rectPaintEvents.size();
- }
+ public abstract boolean hasDrawnCircle();
- public RoundRectPaintHistoryEvent getDrawnRoundRect(int i) {
- return roundRectPaintEvents.get(i);
- }
+ public abstract Paint getDrawnPathPaint(int i);
- public RoundRectPaintHistoryEvent getLastDrawnRoundRect() {
- return roundRectPaintEvents.get(roundRectPaintEvents.size() - 1);
- }
+ public abstract Path getDrawnPath(int i);
- public int getRoundRectPaintHistoryCount() {
- return roundRectPaintEvents.size();
- }
+ public abstract CirclePaintHistoryEvent getDrawnCircle(int i);
- public LinePaintHistoryEvent getDrawnLine(int i) {
- return linePaintEvents.get(i);
- }
+ public abstract ArcPaintHistoryEvent getDrawnArc(int i);
- public int getLinePaintHistoryCount() {
- return linePaintEvents.size();
- }
+ public abstract void resetCanvasHistory();
- public int getOvalPaintHistoryCount() {
- return ovalPaintEvents.size();
- }
+ public abstract Paint getDrawnPaint();
- public OvalPaintHistoryEvent getDrawnOval(int i) {
- return ovalPaintEvents.get(i);
- }
+ public abstract void setHeight(int height);
- @Implementation(maxSdk = N_MR1)
- protected int save() {
- return getNativeCanvas().save();
- }
+ public abstract void setWidth(int width);
- @Implementation(maxSdk = N_MR1)
- protected void restore() {
- getNativeCanvas().restore();
- }
+ public abstract TextHistoryEvent getDrawnTextEvent(int i);
- @Implementation(maxSdk = N_MR1)
- protected int getSaveCount() {
- return getNativeCanvas().getSaveCount();
- }
+ public abstract int getTextHistoryCount();
- @Implementation(maxSdk = N_MR1)
- protected void restoreToCount(int saveCount) {
- getNativeCanvas().restoreToCount(saveCount);
- }
+ public abstract RectPaintHistoryEvent getDrawnRect(int i);
- @Implementation(minSdk = KITKAT)
- protected void release() {
- nativeObjectRegistry.unregister(getNativeId());
- canvasReflector.release();
- }
+ public abstract RectPaintHistoryEvent getLastDrawnRect();
- @Implementation(maxSdk = KITKAT_WATCH)
- protected static int initRaster(int bitmapHandle) {
- return (int) nativeObjectRegistry.register(new NativeCanvas());
- }
+ public abstract int getRectPaintHistoryCount();
- @Implementation(minSdk = LOLLIPOP, maxSdk = LOLLIPOP_MR1)
- protected static long initRaster(long bitmapHandle) {
- return nativeObjectRegistry.register(new NativeCanvas());
- }
+ public abstract RoundRectPaintHistoryEvent getDrawnRoundRect(int i);
- @Implementation(minSdk = M, maxSdk = N_MR1)
- protected static long initRaster(Bitmap bitmap) {
- return nativeObjectRegistry.register(new NativeCanvas());
- }
+ public abstract RoundRectPaintHistoryEvent getLastDrawnRoundRect();
- @Implementation(minSdk = O, maxSdk = P)
- protected static long nInitRaster(Bitmap bitmap) {
- return nativeObjectRegistry.register(new NativeCanvas());
- }
-
- @Implementation(minSdk = Q)
- protected static long nInitRaster(long bitmapHandle) {
- return nativeObjectRegistry.register(new NativeCanvas());
- }
-
- @Implementation(minSdk = O)
- protected static int nGetSaveCount(long canvasHandle) {
- return nativeObjectRegistry.getNativeObject(canvasHandle).getSaveCount();
- }
-
- @Implementation(minSdk = O)
- protected static int nSave(long canvasHandle, int saveFlags) {
- return nativeObjectRegistry.getNativeObject(canvasHandle).save();
- }
-
- @Implementation(maxSdk = KITKAT_WATCH)
- protected static int native_saveLayer(int nativeCanvas, RectF bounds, int paint, int layerFlags) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
-
- @Implementation(maxSdk = KITKAT_WATCH)
- protected static int native_saveLayer(
- int nativeCanvas, float l, float t, float r, float b, int paint, int layerFlags) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
+ public abstract int getRoundRectPaintHistoryCount();
- @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
- protected static int native_saveLayer(
- long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
-
- @Implementation(minSdk = O, maxSdk = R)
- protected static int nSaveLayer(
- long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
+ public abstract LinePaintHistoryEvent getDrawnLine(int i);
- @Implementation(minSdk = S)
- protected static int nSaveLayer(
- long nativeCanvas, float l, float t, float r, float b, long nativePaint) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
+ public abstract int getLinePaintHistoryCount();
- @Implementation(maxSdk = KITKAT_WATCH)
- protected static int native_saveLayerAlpha(
- int nativeCanvas, RectF bounds, int alpha, int layerFlags) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
+ public abstract int getOvalPaintHistoryCount();
- @Implementation(maxSdk = KITKAT_WATCH)
- protected static int native_saveLayerAlpha(
- int nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
-
- @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
- protected static int native_saveLayerAlpha(
- long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
-
- @Implementation(minSdk = O, maxSdk = R)
- protected static int nSaveLayerAlpha(
- long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
-
- @Implementation(minSdk = S)
- protected static int nSaveLayerAlpha(
- long nativeCanvas, float l, float t, float r, float b, int alpha) {
- return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
- }
-
- @Implementation(minSdk = O)
- protected static boolean nRestore(long canvasHandle) {
- return nativeObjectRegistry.getNativeObject(canvasHandle).restore();
- }
-
- @Implementation(minSdk = O)
- protected static void nRestoreToCount(long canvasHandle, int saveCount) {
- nativeObjectRegistry.getNativeObject(canvasHandle).restoreToCount(saveCount);
- }
-
- @Resetter
- public static void reset() {
- nativeObjectRegistry.clear();
- }
+ public abstract OvalPaintHistoryEvent getDrawnOval(int i);
public static class LinePaintHistoryEvent {
public Paint paint;
@@ -585,8 +84,7 @@ public class ShadowCanvas {
public float stopX;
public float stopY;
- private LinePaintHistoryEvent(
- float startX, float startY, float stopX, float stopY, Paint paint) {
+ LinePaintHistoryEvent(float startX, float startY, float stopX, float stopY, Paint paint) {
this.paint = new Paint(paint);
this.paint.setColor(paint.getColor());
this.paint.setStrokeWidth(paint.getStrokeWidth());
@@ -601,7 +99,7 @@ public class ShadowCanvas {
public final RectF oval;
public final Paint paint;
- private OvalPaintHistoryEvent(RectF oval, Paint paint) {
+ OvalPaintHistoryEvent(RectF oval, Paint paint) {
this.oval = new RectF(oval);
this.paint = new Paint(paint);
this.paint.setColor(paint.getColor());
@@ -617,7 +115,7 @@ public class ShadowCanvas {
public final float right;
public final float bottom;
- private RectPaintHistoryEvent(float left, float top, float right, float bottom, Paint paint) {
+ RectPaintHistoryEvent(float left, float top, float right, float bottom, Paint paint) {
this.rect = new RectF(left, top, right, bottom);
this.paint = new Paint(paint);
this.paint.setColor(paint.getColor());
@@ -642,7 +140,7 @@ public class ShadowCanvas {
public final float rx;
public final float ry;
- private RoundRectPaintHistoryEvent(
+ RoundRectPaintHistoryEvent(
float left, float top, float right, float bottom, float rx, float ry, Paint paint) {
this.rect = new RectF(left, top, right, bottom);
this.paint = new Paint(paint);
@@ -659,23 +157,13 @@ public class ShadowCanvas {
}
}
- private static class PathPaintHistoryEvent {
- private final Path drawnPath;
- private final Paint pathPaint;
-
- PathPaintHistoryEvent(Path drawnPath, Paint pathPaint) {
- this.drawnPath = drawnPath;
- this.pathPaint = pathPaint;
- }
- }
-
public static class CirclePaintHistoryEvent {
public final float centerX;
public final float centerY;
public final float radius;
public final Paint paint;
- private CirclePaintHistoryEvent(float centerX, float centerY, float radius, Paint paint) {
+ CirclePaintHistoryEvent(float centerX, float centerY, float radius, Paint paint) {
this.centerX = centerX;
this.centerY = centerY;
this.radius = radius;
@@ -706,7 +194,7 @@ public class ShadowCanvas {
public final Paint paint;
public final String text;
- private TextHistoryEvent(float x, float y, Paint paint, String text) {
+ TextHistoryEvent(float x, float y, Paint paint, String text) {
this.x = x;
this.y = y;
this.paint = paint;
@@ -714,40 +202,11 @@ public class ShadowCanvas {
}
}
- @SuppressWarnings("MemberName")
- @ForType(Canvas.class)
- private interface CanvasReflector {
- @Direct
- void __constructor__(Bitmap bitmap);
-
- @Direct
- void release();
- }
-
- private static class NativeCanvas {
- private int saveCount = 1;
-
- int save() {
- return saveCount++;
- }
-
- boolean restore() {
- if (saveCount > 1) {
- saveCount--;
- return true;
- } else {
- return false;
- }
- }
-
- int getSaveCount() {
- return saveCount;
- }
-
- void restoreToCount(int saveCount) {
- if (saveCount > 0) {
- this.saveCount = saveCount;
- }
+ /** A {@link ShadowPicker} that always selects the legacy ShadowCanvas */
+ public static class CanvasPicker implements ShadowPicker<ShadowCanvas> {
+ @Override
+ public Class<? extends ShadowCanvas> pickShadowClass() {
+ return ShadowLegacyCanvas.class;
}
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
index 8a1721212..39b942857 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCarrierConfigManager.java
@@ -16,6 +16,7 @@ public class ShadowCarrierConfigManager {
private final HashMap<Integer, PersistableBundle> bundles = new HashMap<>();
private final HashMap<Integer, PersistableBundle> overrideBundles = new HashMap<>();
+ private boolean readPhoneStatePermission = true;
/**
* Returns {@link android.os.PersistableBundle} previously set by {@link #overrideConfig} or
@@ -24,6 +25,7 @@ public class ShadowCarrierConfigManager {
*/
@Implementation
public PersistableBundle getConfigForSubId(int subId) {
+ checkReadPhoneStatePermission();
if (overrideBundles.containsKey(subId) && overrideBundles.get(subId) != null) {
return overrideBundles.get(subId);
}
@@ -33,6 +35,10 @@ public class ShadowCarrierConfigManager {
return new PersistableBundle();
}
+ public void setReadPhoneStatePermission(boolean readPhoneStatePermission) {
+ this.readPhoneStatePermission = readPhoneStatePermission;
+ }
+
/**
* Sets that the {@code config} PersistableBundle for a particular {@code subId}; controls the
* return value of {@link CarrierConfigManager#getConfigForSubId()}.
@@ -52,4 +58,10 @@ public class ShadowCarrierConfigManager {
protected void overrideConfig(int subId, @Nullable PersistableBundle config) {
overrideBundles.put(subId, config);
}
+
+ private void checkReadPhoneStatePermission() {
+ if (!readPhoneStatePermission) {
+ throw new SecurityException();
+ }
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java
index 59e275eff..a824e9004 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentProvider.java
@@ -1,6 +1,7 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.Q;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.content.ContentProvider;
@@ -10,14 +11,17 @@ import org.robolectric.annotation.RealObject;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
-@Implements(ContentProvider.class)
+/** Shadow for {@link ContentProvider}. */
+@Implements(value = ContentProvider.class, looseSignatures = true)
public class ShadowContentProvider {
@RealObject private ContentProvider realContentProvider;
private String callingPackage;
- public void setCallingPackage(String callingPackage) {
- this.callingPackage = callingPackage;
+ @Implementation(minSdk = Q, maxSdk = Q)
+ public Object setCallingPackage(Object callingPackage) {
+ this.callingPackage = (String) callingPackage;
+ return callingPackage;
}
@Implementation(minSdk = KITKAT)
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
index 98adb47cf..0b86844e6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowContentResolver.java
@@ -73,20 +73,20 @@ public class ShadowContentResolver {
@RealObject ContentResolver realContentResolver;
private BaseCursor cursor;
- private final List<Statement> statements = new CopyOnWriteArrayList<>();
- private final List<InsertStatement> insertStatements = new CopyOnWriteArrayList<>();
- private final List<UpdateStatement> updateStatements = new CopyOnWriteArrayList<>();
- private final List<DeleteStatement> deleteStatements = new CopyOnWriteArrayList<>();
- private List<NotifiedUri> notifiedUris = new ArrayList<>();
- private Map<Uri, BaseCursor> uriCursorMap = new HashMap<>();
- private Map<Uri, Supplier<InputStream>> inputStreamMap = new HashMap<>();
- private Map<Uri, Supplier<OutputStream>> outputStreamMap = new HashMap<>();
- private final Map<String, List<ContentProviderOperation>> contentProviderOperations =
+ private static final List<Statement> statements = new CopyOnWriteArrayList<>();
+ private static final List<InsertStatement> insertStatements = new CopyOnWriteArrayList<>();
+ private static final List<UpdateStatement> updateStatements = new CopyOnWriteArrayList<>();
+ private static final List<DeleteStatement> deleteStatements = new CopyOnWriteArrayList<>();
+ private static final List<NotifiedUri> notifiedUris = new ArrayList<>();
+ private static final Map<Uri, BaseCursor> uriCursorMap = new HashMap<>();
+ private static final Map<Uri, Supplier<InputStream>> inputStreamMap = new HashMap<>();
+ private static final Map<Uri, Supplier<OutputStream>> outputStreamMap = new HashMap<>();
+ private static final Map<String, List<ContentProviderOperation>> contentProviderOperations =
new HashMap<>();
- private ContentProviderResult[] contentProviderResults;
- private final List<UriPermission> uriPermissions = new ArrayList<>();
+ private static ContentProviderResult[] contentProviderResults;
+ private static final List<UriPermission> uriPermissions = new ArrayList<>();
- private final CopyOnWriteArrayList<ContentObserverEntry> contentObservers =
+ private static final CopyOnWriteArrayList<ContentObserverEntry> contentObservers =
new CopyOnWriteArrayList<>();
private static final Map<String, Map<Account, Status>> syncableAccounts = new HashMap<>();
@@ -98,6 +98,18 @@ public class ShadowContentResolver {
@Resetter
public static void reset() {
+ statements.clear();
+ insertStatements.clear();
+ updateStatements.clear();
+ deleteStatements.clear();
+ notifiedUris.clear();
+ uriCursorMap.clear();
+ inputStreamMap.clear();
+ outputStreamMap.clear();
+ contentProviderOperations.clear();
+ contentProviderResults = null;
+ uriPermissions.clear();
+ contentObservers.clear();
syncableAccounts.clear();
providers.clear();
masterSyncAutomatically = false;
@@ -788,7 +800,7 @@ public class ShadowContentResolver {
*/
@Deprecated
public void setCursor(Uri uri, BaseCursor cursorForUri) {
- this.uriCursorMap.put(uri, cursorForUri);
+ uriCursorMap.put(uri, cursorForUri);
}
/**
@@ -883,7 +895,7 @@ public class ShadowContentResolver {
@Deprecated
public void setContentProviderResult(ContentProviderResult[] contentProviderResults) {
- this.contentProviderResults = contentProviderResults;
+ ShadowContentResolver.contentProviderResults = contentProviderResults;
}
private final Map<Uri, RuntimeException> registerContentProviderExceptions = new HashMap<>();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
index c76888258..dbaa35301 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDevicePolicyManager.java
@@ -1,5 +1,8 @@
package org.robolectric.shadows;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS;
+import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
@@ -128,6 +131,7 @@ public class ShadowDevicePolicyManager {
private final Map<ComponentName, CharSequence> longSupportMessageMap = new HashMap<>();
private final Set<ComponentName> componentsWithActivatedTokens = new HashSet<>();
private Collection<String> packagesToFailForSetApplicationHidden = Collections.emptySet();
+ private int lockTaskFeatures;
private final List<String> lockTaskPackages = new ArrayList<>();
private Context context;
private ApplicationPackageManager applicationPackageManager;
@@ -1236,6 +1240,31 @@ public class ShadowDevicePolicyManager {
return policyGrantedSet != null && policyGrantedSet.contains(usesPolicy);
}
+ @Implementation(minSdk = P)
+ protected int getLockTaskFeatures(ComponentName admin) {
+ Objects.requireNonNull(admin, "ComponentName is null");
+ enforceDeviceOwnerOrProfileOwner(admin);
+ return lockTaskFeatures;
+ }
+
+ @Implementation(minSdk = P)
+ protected void setLockTaskFeatures(ComponentName admin, int flags) {
+ Objects.requireNonNull(admin, "ComponentName is null");
+ enforceDeviceOwnerOrProfileOwner(admin);
+ // Throw if Overview is used without Home.
+ boolean hasHome = (flags & LOCK_TASK_FEATURE_HOME) != 0;
+ boolean hasOverview = (flags & LOCK_TASK_FEATURE_OVERVIEW) != 0;
+ Preconditions.checkArgument(
+ hasHome || !hasOverview,
+ "Cannot use LOCK_TASK_FEATURE_OVERVIEW without LOCK_TASK_FEATURE_HOME");
+ boolean hasNotification = (flags & LOCK_TASK_FEATURE_NOTIFICATIONS) != 0;
+ Preconditions.checkArgument(
+ hasHome || !hasNotification,
+ "Cannot use LOCK_TASK_FEATURE_NOTIFICATIONS without LOCK_TASK_FEATURE_HOME");
+
+ lockTaskFeatures = flags;
+ }
+
@Implementation(minSdk = LOLLIPOP)
protected void setLockTaskPackages(@NonNull ComponentName admin, String[] packages) {
enforceDeviceOwnerOrProfileOwner(admin);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java
index 71b2ca19d..02d2143ac 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayListCanvas.java
@@ -16,7 +16,7 @@ import org.robolectric.annotation.Implements;
isInAndroidSdk = false,
minSdk = M,
maxSdk = R)
-public class ShadowDisplayListCanvas extends ShadowCanvas {
+public class ShadowDisplayListCanvas extends ShadowLegacyCanvas {
@Implementation(minSdk = O, maxSdk = P)
protected static long nCreateDisplayListCanvas(long node, int width, int height) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
index 59dd70783..1675a025a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImsMmTelManager.java
@@ -11,8 +11,9 @@ import android.telephony.SubscriptionManager;
import android.telephony.ims.ImsException;
import android.telephony.ims.ImsMmTelManager;
import android.telephony.ims.ImsMmTelManager.CapabilityCallback;
-import android.telephony.ims.ImsMmTelManager.RegistrationCallback;
import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.RegistrationManager;
import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.ArrayMap;
@@ -43,8 +44,10 @@ public class ShadowImsMmTelManager {
protected static final Map<Integer, ImsMmTelManager> existingInstances = new ArrayMap<>();
- private final Map<RegistrationCallback, Executor> registrationCallbackExecutorMap =
- new ArrayMap<>();
+ private final Map<ImsMmTelManager.RegistrationCallback, Executor>
+ registrationCallbackExecutorMap = new ArrayMap<>();
+ private final Map<RegistrationManager.RegistrationCallback, Executor>
+ registrationManagerCallbackExecutorMap = new ArrayMap<>();
private final Map<CapabilityCallback, Executor> capabilityCallbackExecutorMap = new ArrayMap<>();
private boolean imsAvailableOnDevice = true;
private MmTelCapabilities mmTelCapabilitiesAvailable =
@@ -70,7 +73,7 @@ public class ShadowImsMmTelManager {
@RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@Implementation
protected void registerImsRegistrationCallback(
- @NonNull @CallbackExecutor Executor executor, @NonNull RegistrationCallback c)
+ @NonNull @CallbackExecutor Executor executor, @NonNull ImsMmTelManager.RegistrationCallback c)
throws ImsException {
if (!imsAvailableOnDevice) {
throw new ImsException(
@@ -79,12 +82,41 @@ public class ShadowImsMmTelManager {
registrationCallbackExecutorMap.put(c, executor);
}
+ @RequiresPermission(
+ anyOf = {
+ android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ android.Manifest.permission.READ_PRECISE_PHONE_STATE
+ })
+ @Implementation(minSdk = VERSION_CODES.R)
+ protected void registerImsRegistrationCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull RegistrationManager.RegistrationCallback c)
+ throws ImsException {
+ if (!imsAvailableOnDevice) {
+ throw new ImsException(
+ "IMS not available on device.", ImsException.CODE_ERROR_UNSUPPORTED_OPERATION);
+ }
+ registrationManagerCallbackExecutorMap.put(c, executor);
+ }
+
@RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@Implementation
- protected void unregisterImsRegistrationCallback(@NonNull RegistrationCallback c) {
+ protected void unregisterImsRegistrationCallback(
+ @NonNull ImsMmTelManager.RegistrationCallback c) {
registrationCallbackExecutorMap.remove(c);
}
+ @RequiresPermission(
+ anyOf = {
+ android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+ android.Manifest.permission.READ_PRECISE_PHONE_STATE
+ })
+ @Implementation(minSdk = VERSION_CODES.R)
+ protected void unregisterImsRegistrationCallback(
+ @NonNull RegistrationManager.RegistrationCallback c) {
+ registrationManagerCallbackExecutorMap.remove(c);
+ }
+
/**
* Triggers {@link RegistrationCallback#onRegistering(int)} for all registered {@link
* RegistrationCallback} callbacks.
@@ -92,10 +124,23 @@ public class ShadowImsMmTelManager {
* @see #registerImsRegistrationCallback(Executor, RegistrationCallback)
*/
public void setImsRegistering(int imsRegistrationTech) {
- for (Map.Entry<RegistrationCallback, Executor> entry :
+ for (Map.Entry<ImsMmTelManager.RegistrationCallback, Executor> entry :
registrationCallbackExecutorMap.entrySet()) {
entry.getValue().execute(() -> entry.getKey().onRegistering(imsRegistrationTech));
}
+
+ for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+ registrationManagerCallbackExecutorMap.entrySet()) {
+ entry.getValue().execute(() -> entry.getKey().onRegistering(imsRegistrationTech));
+ }
+ }
+
+ @RequiresApi(api = VERSION_CODES.S)
+ public void setImsRegistering(@NonNull ImsRegistrationAttributes attrs) {
+ for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+ registrationManagerCallbackExecutorMap.entrySet()) {
+ entry.getValue().execute(() -> entry.getKey().onRegistering(attrs));
+ }
}
/**
@@ -106,10 +151,23 @@ public class ShadowImsMmTelManager {
*/
public void setImsRegistered(int imsRegistrationTech) {
this.imsRegistrationTech = imsRegistrationTech;
- for (Map.Entry<RegistrationCallback, Executor> entry :
+ for (Map.Entry<ImsMmTelManager.RegistrationCallback, Executor> entry :
registrationCallbackExecutorMap.entrySet()) {
entry.getValue().execute(() -> entry.getKey().onRegistered(imsRegistrationTech));
}
+
+ for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+ registrationManagerCallbackExecutorMap.entrySet()) {
+ entry.getValue().execute(() -> entry.getKey().onRegistered(imsRegistrationTech));
+ }
+ }
+
+ @RequiresApi(api = VERSION_CODES.S)
+ public void setImsRegistered(@NonNull ImsRegistrationAttributes attrs) {
+ for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+ registrationManagerCallbackExecutorMap.entrySet()) {
+ entry.getValue().execute(() -> entry.getKey().onRegistered(attrs));
+ }
}
/**
@@ -120,10 +178,30 @@ public class ShadowImsMmTelManager {
*/
public void setImsUnregistered(@NonNull ImsReasonInfo imsReasonInfo) {
this.imsRegistrationTech = ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
- for (Map.Entry<RegistrationCallback, Executor> entry :
+ for (Map.Entry<ImsMmTelManager.RegistrationCallback, Executor> entry :
registrationCallbackExecutorMap.entrySet()) {
entry.getValue().execute(() -> entry.getKey().onUnregistered(imsReasonInfo));
}
+
+ for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+ registrationManagerCallbackExecutorMap.entrySet()) {
+ entry.getValue().execute(() -> entry.getKey().onUnregistered(imsReasonInfo));
+ }
+ }
+
+ /**
+ * Triggers {@link RegistrationCallback#onTechnologyChangeFailed(int, ImsReasonInfo)} for all
+ * registered {@link RegistrationCallback} callbacks.
+ *
+ * @see #registerImsRegistrationCallback(Executor, RegistrationCallback)
+ */
+ public void setOnTechnologyChangeFailed(int imsRadioTech, @NonNull ImsReasonInfo imsReasonInfo) {
+ for (Map.Entry<RegistrationManager.RegistrationCallback, Executor> entry :
+ registrationManagerCallbackExecutorMap.entrySet()) {
+ entry
+ .getValue()
+ .execute(() -> entry.getKey().onTechnologyChangeFailed(imsRadioTech, imsReasonInfo));
+ }
}
@RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
@@ -174,7 +252,6 @@ public class ShadowImsMmTelManager {
}
/** Returns only one instance per subscription id. */
- @RequiresApi(api = VERSION_CODES.Q)
@Implementation
protected static ImsMmTelManager createForSubscriptionId(int subId) {
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java
new file mode 100644
index 000000000..1c47aaba2
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInsetsController.java
@@ -0,0 +1,77 @@
+package org.robolectric.shadows;
+
+import android.os.Build;
+import android.view.InsetsController;
+import android.view.WindowInsets;
+import androidx.annotation.RequiresApi;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+
+/** Intercepts calls to [InsetsController] to monitor system bars functionality (hide/show). */
+@Implements(value = InsetsController.class, minSdk = Build.VERSION_CODES.R, isInAndroidSdk = false)
+@RequiresApi(Build.VERSION_CODES.R)
+public class ShadowInsetsController {
+ @ReflectorObject private InsetsControllerReflector insetsControllerReflector;
+
+ /**
+ * Intercepts calls to [InsetsController.show] to detect requested changes to the system
+ * status/nav bar visibility.
+ */
+ @Implementation
+ protected void show(int types) {
+ if (hasStatusBarType(types)) {
+ ShadowViewRootImpl.setIsStatusBarVisible(true);
+ }
+
+ if (hasNavigationBarType(types)) {
+ ShadowViewRootImpl.setIsNavigationBarVisible(true);
+ }
+
+ insetsControllerReflector.show(types);
+ }
+
+ /**
+ * Intercepts calls to [InsetsController.hide] to detect requested changes to the system
+ * status/nav bar visibility.
+ */
+ @Implementation
+ public void hide(int types) {
+ if (hasStatusBarType(types)) {
+ ShadowViewRootImpl.setIsStatusBarVisible(false);
+ }
+
+ if (hasNavigationBarType(types)) {
+ ShadowViewRootImpl.setIsNavigationBarVisible(false);
+ }
+
+ insetsControllerReflector.hide(types);
+ }
+
+ /** Returns true if the given flags contain the mask for the system status bar. */
+ private boolean hasStatusBarType(int types) {
+ return hasTypeMask(types, WindowInsets.Type.statusBars());
+ }
+
+ /** Returns true if the given flags contain the mask for the system navigation bar. */
+ private boolean hasNavigationBarType(int types) {
+ return hasTypeMask(types, WindowInsets.Type.navigationBars());
+ }
+
+ /** Returns true if the given flags contains the requested type mask. */
+ private boolean hasTypeMask(int types, int typeMask) {
+ return (types & typeMask) == typeMask;
+ }
+
+ /** Reflector for [InsetsController] to use for direct (non-intercepted) calls. */
+ @ForType(InsetsController.class)
+ interface InsetsControllerReflector {
+ @Direct
+ void show(int types);
+
+ @Direct
+ void hide(int types);
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLayoutAnimationController.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLayoutAnimationController.java
deleted file mode 100644
index ec6650512..000000000
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLayoutAnimationController.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.robolectric.shadows;
-
-import android.view.animation.LayoutAnimationController;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-
-@Implements(LayoutAnimationController.class)
-public class ShadowLayoutAnimationController {
- @RealObject
- private LayoutAnimationController realAnimation;
-
- private int loadedFromResourceId = -1;
-
- public void setLoadedFromResourceId(int loadedFromResourceId) {
- this.loadedFromResourceId = loadedFromResourceId;
- }
-
- public int getLoadedFromResourceId() {
- if (loadedFromResourceId == -1) {
- throw new IllegalStateException("not loaded from a resource");
- }
- return loadedFromResourceId;
- }
-}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java
new file mode 100644
index 000000000..79b1cbc6b
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyBitmap.java
@@ -0,0 +1,922 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.os.Build.VERSION_CODES.S;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.Integer.max;
+import static java.lang.Integer.min;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Parcel;
+import android.util.DisplayMetrics;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.io.FileDescriptor;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.IntBuffer;
+import java.util.Arrays;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+
+@SuppressWarnings({"UnusedDeclaration"})
+@Implements(value = Bitmap.class, isInAndroidSdk = false)
+public class ShadowLegacyBitmap extends ShadowBitmap {
+ /** Number of bytes used internally to represent each pixel */
+ private static final int INTERNAL_BYTES_PER_PIXEL = 4;
+
+ int createdFromResId = -1;
+ String createdFromPath;
+ InputStream createdFromStream;
+ FileDescriptor createdFromFileDescriptor;
+ byte[] createdFromBytes;
+ @RealObject private Bitmap realBitmap;
+ private Bitmap createdFromBitmap;
+ private Bitmap scaledFromBitmap;
+ private int createdFromX = -1;
+ private int createdFromY = -1;
+ private int createdFromWidth = -1;
+ private int createdFromHeight = -1;
+ private int[] createdFromColors;
+ private Matrix createdFromMatrix;
+ private boolean createdFromFilter;
+
+ private int width;
+ private int height;
+ private BufferedImage bufferedImage;
+ private Bitmap.Config config;
+ private boolean mutable = true;
+ private String description = "";
+ private boolean recycled = false;
+ private boolean hasMipMap;
+ private boolean requestPremultiplied = true;
+ private boolean hasAlpha;
+ private ColorSpace colorSpace;
+
+ @Implementation
+ protected static Bitmap createBitmap(int width, int height, Bitmap.Config config) {
+ return createBitmap((DisplayMetrics) null, width, height, config);
+ }
+
+ @Implementation(minSdk = JELLY_BEAN_MR1)
+ protected static Bitmap createBitmap(
+ DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) {
+ return createBitmap(displayMetrics, width, height, config, true);
+ }
+
+ @Implementation(minSdk = JELLY_BEAN_MR1)
+ protected static Bitmap createBitmap(
+ DisplayMetrics displayMetrics,
+ int width,
+ int height,
+ Bitmap.Config config,
+ boolean hasAlpha) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("width and height must be > 0");
+ }
+ checkNotNull(config);
+ Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
+ shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")");
+
+ shadowBitmap.width = width;
+ shadowBitmap.height = height;
+ shadowBitmap.config = config;
+ shadowBitmap.hasAlpha = hasAlpha;
+ shadowBitmap.setMutable(true);
+ if (displayMetrics != null) {
+ scaledBitmap.setDensity(displayMetrics.densityDpi);
+ }
+ shadowBitmap.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ if (RuntimeEnvironment.getApiLevel() >= O) {
+ shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ }
+ return scaledBitmap;
+ }
+
+ @Implementation(minSdk = O)
+ protected static Bitmap createBitmap(
+ int width, int height, Bitmap.Config config, boolean hasAlpha, ColorSpace colorSpace) {
+ checkArgument(colorSpace != null || config == Bitmap.Config.ALPHA_8);
+ Bitmap bitmap = createBitmap(null, width, height, config, hasAlpha);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
+ shadowBitmap.colorSpace = colorSpace;
+ return bitmap;
+ }
+
+ @Implementation
+ protected static Bitmap createBitmap(
+ Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) {
+ if (x == 0
+ && y == 0
+ && width == src.getWidth()
+ && height == src.getHeight()
+ && (matrix == null || matrix.isIdentity())) {
+ return src; // Return the original.
+ }
+
+ if (x + width > src.getWidth()) {
+ throw new IllegalArgumentException("x + width must be <= bitmap.width()");
+ }
+ if (y + height > src.getHeight()) {
+ throw new IllegalArgumentException("y + height must be <= bitmap.height()");
+ }
+
+ Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+ ShadowLegacyBitmap shadowNewBitmap = Shadow.extract(newBitmap);
+
+ ShadowLegacyBitmap shadowSrcBitmap = Shadow.extract(src);
+ shadowNewBitmap.appendDescription(shadowSrcBitmap.getDescription());
+ shadowNewBitmap.appendDescription(" at (" + x + "," + y + ")");
+ shadowNewBitmap.appendDescription(" with width " + width + " and height " + height);
+
+ shadowNewBitmap.createdFromBitmap = src;
+ shadowNewBitmap.createdFromX = x;
+ shadowNewBitmap.createdFromY = y;
+ shadowNewBitmap.createdFromWidth = width;
+ shadowNewBitmap.createdFromHeight = height;
+ shadowNewBitmap.createdFromMatrix = matrix;
+ shadowNewBitmap.createdFromFilter = filter;
+ shadowNewBitmap.config = src.getConfig();
+ if (matrix != null) {
+ ShadowMatrix shadowMatrix = Shadow.extract(matrix);
+ shadowNewBitmap.appendDescription(" using matrix " + shadowMatrix.getDescription());
+
+ // Adjust width and height by using the matrix.
+ RectF mappedRect = new RectF();
+ matrix.mapRect(mappedRect, new RectF(0, 0, width, height));
+ width = Math.round(mappedRect.width());
+ height = Math.round(mappedRect.height());
+ }
+ if (filter) {
+ shadowNewBitmap.appendDescription(" with filter");
+ }
+
+ // updated if matrix is non-null
+ shadowNewBitmap.width = width;
+ shadowNewBitmap.height = height;
+ shadowNewBitmap.setMutable(true);
+ newBitmap.setDensity(src.getDensity());
+ if ((matrix == null || matrix.isIdentity()) && shadowSrcBitmap.bufferedImage != null) {
+ // Only simple cases are supported for setting image data to the new Bitmap.
+ shadowNewBitmap.bufferedImage =
+ shadowSrcBitmap.bufferedImage.getSubimage(x, y, width, height);
+ }
+ if (RuntimeEnvironment.getApiLevel() >= O) {
+ shadowNewBitmap.colorSpace = shadowSrcBitmap.colorSpace;
+ }
+ return newBitmap;
+ }
+
+ @Implementation
+ protected static Bitmap createBitmap(
+ int[] colors, int offset, int stride, int width, int height, Bitmap.Config config) {
+ return createBitmap(null, colors, offset, stride, width, height, config);
+ }
+
+ @Implementation(minSdk = JELLY_BEAN_MR1)
+ protected static Bitmap createBitmap(
+ DisplayMetrics displayMetrics,
+ int[] colors,
+ int offset,
+ int stride,
+ int width,
+ int height,
+ Bitmap.Config config) {
+ if (width <= 0) {
+ throw new IllegalArgumentException("width must be > 0");
+ }
+ if (height <= 0) {
+ throw new IllegalArgumentException("height must be > 0");
+ }
+ if (Math.abs(stride) < width) {
+ throw new IllegalArgumentException("abs(stride) must be >= width");
+ }
+ checkNotNull(config);
+ int lastScanline = offset + (height - 1) * stride;
+ int length = colors.length;
+ if (offset < 0
+ || (offset + width > length)
+ || lastScanline < 0
+ || (lastScanline + width > length)) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+
+ BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ bufferedImage.setRGB(0, 0, width, height, colors, offset, stride);
+ Bitmap bitmap = createBitmap(bufferedImage, width, height, config);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap);
+ shadowBitmap.setMutable(false);
+ shadowBitmap.createdFromColors = colors;
+ if (displayMetrics != null) {
+ bitmap.setDensity(displayMetrics.densityDpi);
+ }
+ if (RuntimeEnvironment.getApiLevel() >= O) {
+ shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
+ }
+ return bitmap;
+ }
+
+ private static Bitmap createBitmap(
+ BufferedImage bufferedImage, int width, int height, Bitmap.Config config) {
+ Bitmap newBitmap = Bitmap.createBitmap(width, height, config);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap);
+ shadowBitmap.bufferedImage = bufferedImage;
+ return newBitmap;
+ }
+
+ @Implementation
+ protected static Bitmap createScaledBitmap(
+ Bitmap src, int dstWidth, int dstHeight, boolean filter) {
+ if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) {
+ return src; // Return the original.
+ }
+ if (dstWidth <= 0 || dstHeight <= 0) {
+ throw new IllegalArgumentException("width and height must be > 0");
+ }
+ Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap);
+
+ ShadowLegacyBitmap shadowSrcBitmap = Shadow.extract(src);
+ shadowBitmap.appendDescription(shadowSrcBitmap.getDescription());
+ shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight);
+ if (filter) {
+ shadowBitmap.appendDescription(" with filter " + filter);
+ }
+
+ shadowBitmap.createdFromBitmap = src;
+ shadowBitmap.scaledFromBitmap = src;
+ shadowBitmap.createdFromFilter = filter;
+ shadowBitmap.width = dstWidth;
+ shadowBitmap.height = dstHeight;
+ shadowBitmap.config = src.getConfig();
+ shadowBitmap.mutable = true;
+ if (!ImageUtil.scaledBitmap(src, scaledBitmap, filter)) {
+ shadowBitmap.bufferedImage =
+ new BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB);
+ shadowBitmap.setPixelsInternal(
+ new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()],
+ 0,
+ 0,
+ 0,
+ 0,
+ shadowBitmap.getWidth(),
+ shadowBitmap.getHeight());
+ }
+ if (RuntimeEnvironment.getApiLevel() >= O) {
+ shadowBitmap.colorSpace = shadowSrcBitmap.colorSpace;
+ }
+ return scaledBitmap;
+ }
+
+ @Implementation
+ protected static Bitmap nativeCreateFromParcel(Parcel p) {
+ int parceledWidth = p.readInt();
+ int parceledHeight = p.readInt();
+ Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable();
+
+ int[] parceledColors = new int[parceledHeight * parceledWidth];
+ p.readIntArray(parceledColors);
+
+ return createBitmap(
+ parceledColors, 0, parceledWidth, parceledWidth, parceledHeight, parceledConfig);
+ }
+
+ static int getBytesPerPixel(Bitmap.Config config) {
+ if (config == null) {
+ throw new NullPointerException("Bitmap config was null.");
+ }
+ switch (config) {
+ case RGBA_F16:
+ return 8;
+ case ARGB_8888:
+ case HARDWARE:
+ return 4;
+ case RGB_565:
+ case ARGB_4444:
+ return 2;
+ case ALPHA_8:
+ return 1;
+ default:
+ throw new IllegalArgumentException("Unknown bitmap config: " + config);
+ }
+ }
+
+ /**
+ * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap
+ * was not copied from another instance.
+ *
+ * @return Original Bitmap from which this Bitmap was created.
+ */
+ @Override
+ public Bitmap getCreatedFromBitmap() {
+ return createdFromBitmap;
+ }
+
+ /**
+ * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created from a
+ * resource.
+ *
+ * @return Resource ID from which this Bitmap was created.
+ */
+ @Override
+ public int getCreatedFromResId() {
+ return createdFromResId;
+ }
+
+ /**
+ * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a
+ * path.
+ *
+ * @return Path from which this Bitmap was created.
+ */
+ @Override
+ public String getCreatedFromPath() {
+ return createdFromPath;
+ }
+
+ /**
+ * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not
+ * created from a stream.
+ *
+ * @return InputStream from which this Bitmap was created.
+ */
+ @Override
+ public InputStream getCreatedFromStream() {
+ return createdFromStream;
+ }
+
+ /**
+ * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from
+ * bytes.
+ *
+ * @return Bytes from which this Bitmap was created.
+ */
+ @Override
+ public byte[] getCreatedFromBytes() {
+ return createdFromBytes;
+ }
+
+ /**
+ * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
+ *
+ * @return Horizontal offset within {@link #getCreatedFromBitmap()}.
+ */
+ @Override
+ public int getCreatedFromX() {
+ return createdFromX;
+ }
+
+ /**
+ * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
+ *
+ * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1.
+ */
+ @Override
+ public int getCreatedFromY() {
+ return createdFromY;
+ }
+
+ /**
+ * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
+ * content, or -1.
+ *
+ * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
+ * Bitmap's content, or -1.
+ */
+ @Override
+ public int getCreatedFromWidth() {
+ return createdFromWidth;
+ }
+
+ /**
+ * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's
+ * content, or -1.
+ *
+ * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this
+ * Bitmap's content, or -1.
+ */
+ @Override
+ public int getCreatedFromHeight() {
+ return createdFromHeight;
+ }
+
+ /**
+ * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created
+ * from a color array.
+ *
+ * @return Color array from which this Bitmap was created.
+ */
+ @Override
+ public int[] getCreatedFromColors() {
+ return createdFromColors;
+ }
+
+ /**
+ * Matrix from which this Bitmap's content was transformed, or {@code null}.
+ *
+ * @return Matrix from which this Bitmap's content was transformed, or {@code null}.
+ */
+ @Override
+ public Matrix getCreatedFromMatrix() {
+ return createdFromMatrix;
+ }
+
+ /**
+ * {@code true} if this Bitmap was created with filtering.
+ *
+ * @return {@code true} if this Bitmap was created with filtering.
+ */
+ @Override
+ public boolean getCreatedFromFilter() {
+ return createdFromFilter;
+ }
+
+ @Implementation(minSdk = S)
+ protected Bitmap asShared() {
+ setMutable(false);
+ return realBitmap;
+ }
+
+ @Implementation
+ protected boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) {
+ appendDescription(" compressed as " + format + " with quality " + quality);
+ return ImageUtil.writeToStream(realBitmap, format, quality, stream);
+ }
+
+ @Implementation
+ protected void setPixels(
+ int[] pixels, int offset, int stride, int x, int y, int width, int height) {
+ checkBitmapMutable();
+ setPixelsInternal(pixels, offset, stride, x, y, width, height);
+ }
+
+ void setPixelsInternal(
+ int[] pixels, int offset, int stride, int x, int y, int width, int height) {
+ if (bufferedImage == null) {
+ bufferedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
+ }
+ bufferedImage.setRGB(x, y, width, height, pixels, offset, stride);
+ }
+
+ @Implementation
+ protected int getPixel(int x, int y) {
+ internalCheckPixelAccess(x, y);
+ if (bufferedImage != null) {
+ // Note that getPixel() returns a non-premultiplied ARGB value; if
+ // config is RGB_565, our return value will likely be more precise than
+ // on a physical device, since it needs to map each color component from
+ // 5 or 6 bits to 8 bits.
+ return bufferedImage.getRGB(x, y);
+ } else {
+ return 0;
+ }
+ }
+
+ @Implementation
+ protected void setPixel(int x, int y, int color) {
+ checkBitmapMutable();
+ internalCheckPixelAccess(x, y);
+ if (bufferedImage == null) {
+ bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ }
+ bufferedImage.setRGB(x, y, color);
+ }
+
+ /**
+ * Note that this method will return a RuntimeException unless: - {@code pixels} has the same
+ * length as the number of pixels of the bitmap. - {@code x = 0} - {@code y = 0} - {@code width}
+ * and {@code height} height match the current bitmap's dimensions.
+ */
+ @Implementation
+ protected void getPixels(
+ int[] pixels, int offset, int stride, int x, int y, int width, int height) {
+ bufferedImage.getRGB(x, y, width, height, pixels, offset, stride);
+ }
+
+ @Implementation
+ protected int getRowBytes() {
+ return getBytesPerPixel(config) * getWidth();
+ }
+
+ @Implementation
+ protected int getByteCount() {
+ return getRowBytes() * getHeight();
+ }
+
+ @Implementation
+ protected void recycle() {
+ recycled = true;
+ }
+
+ @Implementation
+ protected final boolean isRecycled() {
+ return recycled;
+ }
+
+ @Implementation
+ protected Bitmap copy(Bitmap.Config config, boolean isMutable) {
+ Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+ ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap);
+ shadowBitmap.createdFromBitmap = realBitmap;
+ shadowBitmap.config = config;
+ shadowBitmap.mutable = isMutable;
+ shadowBitmap.height = getHeight();
+ shadowBitmap.width = getWidth();
+ if (bufferedImage != null) {
+ ColorModel cm = bufferedImage.getColorModel();
+ WritableRaster raster =
+ bufferedImage.copyData(bufferedImage.getRaster().createCompatibleWritableRaster());
+ shadowBitmap.bufferedImage = new BufferedImage(cm, raster, false, null);
+ }
+ return newBitmap;
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected final int getAllocationByteCount() {
+ return getRowBytes() * getHeight();
+ }
+
+ @Implementation
+ protected final Bitmap.Config getConfig() {
+ return config;
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected void setConfig(Bitmap.Config config) {
+ this.config = config;
+ }
+
+ @Implementation
+ protected final boolean isMutable() {
+ return mutable;
+ }
+
+ @Override
+ public void setMutable(boolean mutable) {
+ this.mutable = mutable;
+ }
+
+ @Override
+ public void appendDescription(String s) {
+ description += s;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public void setDescription(String s) {
+ description = s;
+ }
+
+ @Implementation
+ protected final boolean hasAlpha() {
+ return hasAlpha && config != Bitmap.Config.RGB_565;
+ }
+
+ @Implementation
+ protected void setHasAlpha(boolean hasAlpha) {
+ this.hasAlpha = hasAlpha;
+ }
+
+ @Implementation
+ protected Bitmap extractAlpha() {
+ WritableRaster raster = bufferedImage.getAlphaRaster();
+ BufferedImage alphaImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ alphaImage.getAlphaRaster().setRect(raster);
+ return createBitmap(alphaImage, getWidth(), getHeight(), Bitmap.Config.ALPHA_8);
+ }
+
+ /**
+ * This shadow implementation ignores the given paint and offsetXY and simply calls {@link
+ * #extractAlpha()}.
+ */
+ @Implementation
+ protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
+ return extractAlpha();
+ }
+
+ @Implementation(minSdk = JELLY_BEAN_MR1)
+ protected final boolean hasMipMap() {
+ return hasMipMap;
+ }
+
+ @Implementation(minSdk = JELLY_BEAN_MR1)
+ protected final void setHasMipMap(boolean hasMipMap) {
+ this.hasMipMap = hasMipMap;
+ }
+
+ @Implementation
+ protected int getWidth() {
+ return width;
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected void setWidth(int width) {
+ this.width = width;
+ }
+
+ @Implementation
+ protected int getHeight() {
+ return height;
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected void setHeight(int height) {
+ this.height = height;
+ }
+
+ @Implementation
+ protected int getGenerationId() {
+ return 0;
+ }
+
+ @Implementation(minSdk = M)
+ protected Bitmap createAshmemBitmap() {
+ return realBitmap;
+ }
+
+ @Implementation
+ protected void eraseColor(int color) {
+ if (bufferedImage != null) {
+ int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+ Arrays.fill(pixels, color);
+ }
+ setDescription(String.format("Bitmap (%d, %d)", width, height));
+ if (color != 0) {
+ appendDescription(String.format(" erased with 0x%08x", color));
+ }
+ }
+
+ @Implementation
+ protected void writeToParcel(Parcel p, int flags) {
+ p.writeInt(width);
+ p.writeInt(height);
+ p.writeSerializable(config);
+ int[] pixels = new int[width * height];
+ getPixels(pixels, 0, width, 0, 0, width, height);
+ p.writeIntArray(pixels);
+ }
+
+ @Implementation
+ protected void copyPixelsFromBuffer(Buffer dst) {
+ if (isRecycled()) {
+ throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap");
+ }
+
+ // See the related comment in #copyPixelsToBuffer(Buffer).
+ if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
+ throw new RuntimeException(
+ "Not implemented: only Bitmaps with "
+ + INTERNAL_BYTES_PER_PIXEL
+ + " bytes per pixel are supported");
+ }
+ if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
+ throw new RuntimeException("Not implemented: unsupported Buffer subclass");
+ }
+
+ ByteBuffer byteBuffer = null;
+ IntBuffer intBuffer;
+ if (dst instanceof IntBuffer) {
+ intBuffer = (IntBuffer) dst;
+ } else {
+ byteBuffer = (ByteBuffer) dst;
+ intBuffer = byteBuffer.asIntBuffer();
+ }
+
+ if (intBuffer.remaining() < (width * height)) {
+ throw new RuntimeException("Buffer not large enough for pixels");
+ }
+
+ int[] colors = new int[width * height];
+ intBuffer.get(colors);
+ if (byteBuffer != null) {
+ byteBuffer.position(byteBuffer.position() + intBuffer.position() * INTERNAL_BYTES_PER_PIXEL);
+ }
+ int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+ System.arraycopy(colors, 0, pixels, 0, pixels.length);
+ }
+
+ @Implementation
+ protected void copyPixelsToBuffer(Buffer dst) {
+ // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels
+ // internally. Clients of this API probably expect that the buffer size must be >=
+ // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other
+ // configs that value would be smaller then the buffer size we actually need.
+ if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) {
+ throw new RuntimeException(
+ "Not implemented: only Bitmaps with "
+ + INTERNAL_BYTES_PER_PIXEL
+ + " bytes per pixel are supported");
+ }
+
+ if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) {
+ throw new RuntimeException("Not implemented: unsupported Buffer subclass");
+ }
+ int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+ if (dst instanceof ByteBuffer) {
+ IntBuffer intBuffer = ((ByteBuffer) dst).asIntBuffer();
+ intBuffer.put(pixels);
+ dst.position(intBuffer.position() * 4);
+ } else if (dst instanceof IntBuffer) {
+ ((IntBuffer) dst).put(pixels);
+ }
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected void reconfigure(int width, int height, Bitmap.Config config) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.config == Bitmap.Config.HARDWARE) {
+ throw new IllegalStateException("native-backed bitmaps may not be reconfigured");
+ }
+
+ // This should throw if the resulting allocation size is greater than the initial allocation
+ // size of our Bitmap, but we don't keep track of that information reliably, so we're forced to
+ // assume that our original dimensions and config are large enough to fit the new dimensions and
+ // config
+ this.width = width;
+ this.height = height;
+ this.config = config;
+ bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected boolean isPremultiplied() {
+ return requestPremultiplied && hasAlpha();
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected void setPremultiplied(boolean isPremultiplied) {
+ this.requestPremultiplied = isPremultiplied;
+ }
+
+ @Implementation(minSdk = O)
+ protected ColorSpace getColorSpace() {
+ return colorSpace;
+ }
+
+ @Implementation(minSdk = Q)
+ protected void setColorSpace(ColorSpace colorSpace) {
+ this.colorSpace = checkNotNull(colorSpace);
+ }
+
+ @Implementation
+ protected boolean sameAs(Bitmap other) {
+ if (other == null) {
+ return false;
+ }
+ ShadowLegacyBitmap shadowOtherBitmap = Shadow.extract(other);
+ if (this.width != shadowOtherBitmap.width || this.height != shadowOtherBitmap.height) {
+ return false;
+ }
+ if (this.config != shadowOtherBitmap.config) {
+ return false;
+ }
+
+ if (bufferedImage == null && shadowOtherBitmap.bufferedImage != null) {
+ return false;
+ } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage == null) {
+ return false;
+ } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage != null) {
+ int[] pixels = ((DataBufferInt) bufferedImage.getData().getDataBuffer()).getData();
+ int[] otherPixels =
+ ((DataBufferInt) shadowOtherBitmap.bufferedImage.getData().getDataBuffer()).getData();
+ if (!Arrays.equals(pixels, otherPixels)) {
+ return false;
+ }
+ }
+ // When Bitmap.createScaledBitmap is called, the colors array is cleared, so we need a basic
+ // way to detect if two scaled bitmaps are the same.
+ if (scaledFromBitmap != null && shadowOtherBitmap.scaledFromBitmap != null) {
+ return scaledFromBitmap.sameAs(shadowOtherBitmap.scaledFromBitmap);
+ }
+ return true;
+ }
+
+ void setCreatedFromResId(int resId, String description) {
+ this.createdFromResId = resId;
+ appendDescription(" for resource:" + description);
+ }
+
+ private void checkBitmapMutable() {
+ if (isRecycled()) {
+ throw new IllegalStateException("Can't call setPixel() on a recycled bitmap");
+ } else if (!isMutable()) {
+ throw new IllegalStateException("Bitmap is immutable");
+ }
+ }
+
+ private void internalCheckPixelAccess(int x, int y) {
+ if (x < 0) {
+ throw new IllegalArgumentException("x must be >= 0");
+ }
+ if (y < 0) {
+ throw new IllegalArgumentException("y must be >= 0");
+ }
+ if (x >= getWidth()) {
+ throw new IllegalArgumentException("x must be < bitmap.width()");
+ }
+ if (y >= getHeight()) {
+ throw new IllegalArgumentException("y must be < bitmap.height()");
+ }
+ }
+
+ void drawRect(Rect r, Paint paint) {
+ if (bufferedImage == null) {
+ return;
+ }
+ int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+
+ Rect toDraw =
+ new Rect(
+ max(0, r.left), max(0, r.top), min(getWidth(), r.right), min(getHeight(), r.bottom));
+ if (toDraw.left == 0 && toDraw.top == 0 && toDraw.right == getWidth()) {
+ Arrays.fill(pixels, 0, getWidth() * toDraw.bottom, paint.getColor());
+ return;
+ }
+ for (int y = toDraw.top; y < toDraw.bottom; y++) {
+ Arrays.fill(
+ pixels, y * getWidth() + toDraw.left, y * getWidth() + toDraw.right, paint.getColor());
+ }
+ }
+
+ void drawRect(RectF r, Paint paint) {
+ if (bufferedImage == null) {
+ return;
+ }
+
+ Graphics2D graphics2D = bufferedImage.createGraphics();
+ Rectangle2D r2d = new Rectangle2D.Float(r.left, r.top, r.right - r.left, r.bottom - r.top);
+ graphics2D.setColor(new Color(paint.getColor()));
+ graphics2D.draw(r2d);
+ graphics2D.dispose();
+ }
+
+ void drawBitmap(Bitmap source, int left, int top) {
+ ShadowLegacyBitmap shadowSource = Shadow.extract(source);
+ if (bufferedImage == null || shadowSource.bufferedImage == null) {
+ // pixel data not available, so there's nothing we can do
+ return;
+ }
+
+ int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+ int[] sourcePixels =
+ ((DataBufferInt) shadowSource.bufferedImage.getRaster().getDataBuffer()).getData();
+
+ // fast path
+ if (left == 0 && top == 0 && getWidth() == source.getWidth()) {
+ int size = min(getWidth() * getHeight(), source.getWidth() * source.getHeight());
+ System.arraycopy(sourcePixels, 0, pixels, 0, size);
+ return;
+ }
+ // slower (row-by-row) path
+ int startSourceY = max(0, -top);
+ int startSourceX = max(0, -left);
+ int startY = max(0, top);
+ int startX = max(0, left);
+ int endY = min(getHeight(), top + source.getHeight());
+ int endX = min(getWidth(), left + source.getWidth());
+ int lenY = endY - startY;
+ int lenX = endX - startX;
+ for (int y = 0; y < lenY; y++) {
+ System.arraycopy(
+ sourcePixels,
+ (startSourceY + y) * source.getWidth() + startSourceX,
+ pixels,
+ (startY + y) * getWidth() + startX,
+ lenX);
+ }
+ }
+
+ BufferedImage getBufferedImage() {
+ return bufferedImage;
+ }
+
+ void setBufferedImage(BufferedImage bufferedImage) {
+ this.bufferedImage = bufferedImage;
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCanvas.java
new file mode 100644
index 000000000..9c63ed3d4
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyCanvas.java
@@ -0,0 +1,642 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+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_MR1;
+import static android.os.Build.VERSION_CODES.O;
+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 android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import com.google.common.base.Preconditions;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.res.android.NativeObjRegistry;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
+
+/**
+ * Broken. This implementation is very specific to the application for which it was developed. Todo:
+ * Reimplement. Consider using the same strategy of collecting a history of draw events and
+ * providing methods for writing queries based on type, number, and order of events.
+ */
+@SuppressWarnings({"UnusedDeclaration"})
+@Implements(value = Canvas.class, isInAndroidSdk = false)
+public class ShadowLegacyCanvas extends ShadowCanvas {
+ private static final NativeObjRegistry<NativeCanvas> nativeObjectRegistry =
+ new NativeObjRegistry<>(NativeCanvas.class);
+
+ @RealObject protected Canvas realCanvas;
+ @ReflectorObject protected CanvasReflector canvasReflector;
+
+ private final List<RoundRectPaintHistoryEvent> roundRectPaintEvents = new ArrayList<>();
+ private List<PathPaintHistoryEvent> pathPaintEvents = new ArrayList<>();
+ private List<CirclePaintHistoryEvent> circlePaintEvents = new ArrayList<>();
+ private List<ArcPaintHistoryEvent> arcPaintEvents = new ArrayList<>();
+ private List<RectPaintHistoryEvent> rectPaintEvents = new ArrayList<>();
+ private List<LinePaintHistoryEvent> linePaintEvents = new ArrayList<>();
+ private List<OvalPaintHistoryEvent> ovalPaintEvents = new ArrayList<>();
+ private List<TextHistoryEvent> drawnTextEventHistory = new ArrayList<>();
+ private Paint drawnPaint;
+ private Bitmap targetBitmap = ReflectionHelpers.callConstructor(Bitmap.class);
+ private float translateX;
+ private float translateY;
+ private float scaleX = 1;
+ private float scaleY = 1;
+ private int height;
+ private int width;
+
+ @Implementation
+ protected void __constructor__(Bitmap bitmap) {
+ canvasReflector.__constructor__(bitmap);
+ this.targetBitmap = bitmap;
+ }
+
+ private long getNativeId() {
+ return RuntimeEnvironment.getApiLevel() <= KITKAT_WATCH
+ ? (int) ReflectionHelpers.getField(realCanvas, "mNativeCanvas")
+ : realCanvas.getNativeCanvasWrapper();
+ }
+
+ private NativeCanvas getNativeCanvas() {
+ return nativeObjectRegistry.getNativeObject(getNativeId());
+ }
+
+ @Override
+ public void appendDescription(String s) {
+ ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
+ shadowBitmap.appendDescription(s);
+ }
+
+ @Override
+ public String getDescription() {
+ ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
+ return shadowBitmap.getDescription();
+ }
+
+ @Implementation
+ protected void setBitmap(Bitmap bitmap) {
+ targetBitmap = bitmap;
+ }
+
+ @Implementation
+ protected void drawText(String text, float x, float y, Paint paint) {
+ drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text));
+ }
+
+ @Implementation
+ protected void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) {
+ drawnTextEventHistory.add(
+ new TextHistoryEvent(x, y, paint, text.subSequence(start, end).toString()));
+ }
+
+ @Implementation
+ protected void drawText(char[] text, int index, int count, float x, float y, Paint paint) {
+ drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, new String(text, index, count)));
+ }
+
+ @Implementation
+ protected void drawText(String text, int start, int end, float x, float y, Paint paint) {
+ drawnTextEventHistory.add(new TextHistoryEvent(x, y, paint, text.substring(start, end)));
+ }
+
+ @Implementation
+ protected void translate(float x, float y) {
+ this.translateX = x;
+ this.translateY = y;
+ }
+
+ @Implementation
+ protected void scale(float sx, float sy) {
+ this.scaleX = sx;
+ this.scaleY = sy;
+ }
+
+ @Implementation
+ protected void scale(float sx, float sy, float px, float py) {
+ this.scaleX = sx;
+ this.scaleY = sy;
+ }
+
+ @Implementation
+ protected void drawPaint(Paint paint) {
+ drawnPaint = paint;
+ }
+
+ @Implementation
+ protected void drawColor(int color) {
+ appendDescription("draw color " + color);
+ }
+
+ @Implementation
+ protected void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) {
+ describeBitmap(bitmap, paint);
+
+ int x = (int) (left + translateX);
+ int y = (int) (top + translateY);
+ if (x != 0 || y != 0) {
+ appendDescription(" at (" + x + "," + y + ")");
+ }
+
+ if (scaleX != 1 && scaleY != 1) {
+ appendDescription(" scaled by (" + scaleX + "," + scaleY + ")");
+ }
+
+ if (bitmap != null && targetBitmap != null) {
+ ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
+ shadowTargetBitmap.drawBitmap(bitmap, (int) left, (int) top);
+ }
+ }
+
+ @Implementation
+ protected void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) {
+ describeBitmap(bitmap, paint);
+
+ StringBuilder descriptionBuilder = new StringBuilder();
+ if (dst != null) {
+ descriptionBuilder
+ .append(" at (")
+ .append(dst.left)
+ .append(",")
+ .append(dst.top)
+ .append(") with height=")
+ .append(dst.height())
+ .append(" and width=")
+ .append(dst.width());
+ }
+
+ if (src != null) {
+ descriptionBuilder.append(" taken from ").append(src.toString());
+ }
+ appendDescription(descriptionBuilder.toString());
+ }
+
+ @Implementation
+ protected void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) {
+ describeBitmap(bitmap, paint);
+
+ StringBuilder descriptionBuilder = new StringBuilder();
+ if (dst != null) {
+ descriptionBuilder
+ .append(" at (")
+ .append(dst.left)
+ .append(",")
+ .append(dst.top)
+ .append(") with height=")
+ .append(dst.height())
+ .append(" and width=")
+ .append(dst.width());
+ }
+
+ if (src != null) {
+ descriptionBuilder.append(" taken from ").append(src.toString());
+ }
+ appendDescription(descriptionBuilder.toString());
+ }
+
+ @Implementation
+ protected void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) {
+ describeBitmap(bitmap, paint);
+
+ ShadowMatrix shadowMatrix = Shadow.extract(matrix);
+ appendDescription(" transformed by " + shadowMatrix.getDescription());
+ }
+
+ @Implementation
+ protected void drawPath(Path path, Paint paint) {
+ pathPaintEvents.add(new PathPaintHistoryEvent(new Path(path), new Paint(paint)));
+
+ separateLines();
+ ShadowPath shadowPath = Shadow.extract(path);
+ appendDescription("Path " + shadowPath.getPoints().toString());
+ }
+
+ @Implementation
+ protected void drawCircle(float cx, float cy, float radius, Paint paint) {
+ circlePaintEvents.add(new CirclePaintHistoryEvent(cx, cy, radius, paint));
+ }
+
+ @Implementation
+ protected void drawArc(
+ RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) {
+ arcPaintEvents.add(new ArcPaintHistoryEvent(oval, startAngle, sweepAngle, useCenter, paint));
+ }
+
+ @Implementation
+ protected void drawRect(float left, float top, float right, float bottom, Paint paint) {
+ rectPaintEvents.add(new RectPaintHistoryEvent(left, top, right, bottom, paint));
+
+ if (targetBitmap != null) {
+ ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
+ shadowTargetBitmap.drawRect(new RectF(left, top, right, bottom), paint);
+ }
+ }
+
+ @Implementation
+ protected void drawRect(Rect r, Paint paint) {
+ rectPaintEvents.add(new RectPaintHistoryEvent(r.left, r.top, r.right, r.bottom, paint));
+
+ if (targetBitmap != null) {
+ ShadowLegacyBitmap shadowTargetBitmap = Shadow.extract(targetBitmap);
+ shadowTargetBitmap.drawRect(r, paint);
+ }
+ }
+
+ @Implementation
+ protected void drawRoundRect(RectF rect, float rx, float ry, Paint paint) {
+ roundRectPaintEvents.add(
+ new RoundRectPaintHistoryEvent(
+ rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint));
+ }
+
+ @Implementation
+ protected void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
+ linePaintEvents.add(new LinePaintHistoryEvent(startX, startY, stopX, stopY, paint));
+ }
+
+ @Implementation
+ protected void drawOval(RectF oval, Paint paint) {
+ ovalPaintEvents.add(new OvalPaintHistoryEvent(oval, paint));
+ }
+
+ private void describeBitmap(Bitmap bitmap, Paint paint) {
+ separateLines();
+
+ ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ appendDescription(shadowBitmap.getDescription());
+
+ if (paint != null) {
+ ColorFilter colorFilter = paint.getColorFilter();
+ if (colorFilter != null) {
+ appendDescription(" with " + colorFilter.getClass().getSimpleName());
+ }
+ }
+ }
+
+ private void separateLines() {
+ if (getDescription().length() != 0) {
+ appendDescription("\n");
+ }
+ }
+
+ @Override
+ public int getPathPaintHistoryCount() {
+ return pathPaintEvents.size();
+ }
+
+ @Override
+ public int getCirclePaintHistoryCount() {
+ return circlePaintEvents.size();
+ }
+
+ @Override
+ public int getArcPaintHistoryCount() {
+ return arcPaintEvents.size();
+ }
+
+ @Override
+ public boolean hasDrawnPath() {
+ return getPathPaintHistoryCount() > 0;
+ }
+
+ @Override
+ public boolean hasDrawnCircle() {
+ return circlePaintEvents.size() > 0;
+ }
+
+ @Override
+ public Paint getDrawnPathPaint(int i) {
+ return pathPaintEvents.get(i).pathPaint;
+ }
+
+ @Override
+ public Path getDrawnPath(int i) {
+ return pathPaintEvents.get(i).drawnPath;
+ }
+
+ @Override
+ public CirclePaintHistoryEvent getDrawnCircle(int i) {
+ return circlePaintEvents.get(i);
+ }
+
+ @Override
+ public ArcPaintHistoryEvent getDrawnArc(int i) {
+ return arcPaintEvents.get(i);
+ }
+
+ @Override
+ public void resetCanvasHistory() {
+ drawnTextEventHistory.clear();
+ pathPaintEvents.clear();
+ circlePaintEvents.clear();
+ rectPaintEvents.clear();
+ roundRectPaintEvents.clear();
+ linePaintEvents.clear();
+ ovalPaintEvents.clear();
+ ShadowBitmap shadowBitmap = Shadow.extract(targetBitmap);
+ shadowBitmap.setDescription("");
+ }
+
+ @Override
+ public Paint getDrawnPaint() {
+ return drawnPaint;
+ }
+
+ @Override
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ @Override
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ @Implementation
+ protected int getWidth() {
+ if (width == 0) {
+ return targetBitmap.getWidth();
+ }
+ return width;
+ }
+
+ @Implementation
+ protected int getHeight() {
+ if (height == 0) {
+ return targetBitmap.getHeight();
+ }
+ return height;
+ }
+
+ @Implementation
+ protected boolean getClipBounds(Rect bounds) {
+ Preconditions.checkNotNull(bounds);
+ if (targetBitmap == null) {
+ return false;
+ }
+ bounds.set(0, 0, targetBitmap.getWidth(), targetBitmap.getHeight());
+ return !bounds.isEmpty();
+ }
+
+ @Override
+ public TextHistoryEvent getDrawnTextEvent(int i) {
+ return drawnTextEventHistory.get(i);
+ }
+
+ @Override
+ public int getTextHistoryCount() {
+ return drawnTextEventHistory.size();
+ }
+
+ @Override
+ public RectPaintHistoryEvent getDrawnRect(int i) {
+ return rectPaintEvents.get(i);
+ }
+
+ @Override
+ public RectPaintHistoryEvent getLastDrawnRect() {
+ return rectPaintEvents.get(rectPaintEvents.size() - 1);
+ }
+
+ @Override
+ public int getRectPaintHistoryCount() {
+ return rectPaintEvents.size();
+ }
+
+ @Override
+ public RoundRectPaintHistoryEvent getDrawnRoundRect(int i) {
+ return roundRectPaintEvents.get(i);
+ }
+
+ @Override
+ public RoundRectPaintHistoryEvent getLastDrawnRoundRect() {
+ return roundRectPaintEvents.get(roundRectPaintEvents.size() - 1);
+ }
+
+ @Override
+ public int getRoundRectPaintHistoryCount() {
+ return roundRectPaintEvents.size();
+ }
+
+ @Override
+ public LinePaintHistoryEvent getDrawnLine(int i) {
+ return linePaintEvents.get(i);
+ }
+
+ @Override
+ public int getLinePaintHistoryCount() {
+ return linePaintEvents.size();
+ }
+
+ @Override
+ public int getOvalPaintHistoryCount() {
+ return ovalPaintEvents.size();
+ }
+
+ @Override
+ public OvalPaintHistoryEvent getDrawnOval(int i) {
+ return ovalPaintEvents.get(i);
+ }
+
+ @Implementation(maxSdk = N_MR1)
+ protected int save() {
+ return getNativeCanvas().save();
+ }
+
+ @Implementation(maxSdk = N_MR1)
+ protected void restore() {
+ getNativeCanvas().restore();
+ }
+
+ @Implementation(maxSdk = N_MR1)
+ protected int getSaveCount() {
+ return getNativeCanvas().getSaveCount();
+ }
+
+ @Implementation(maxSdk = N_MR1)
+ protected void restoreToCount(int saveCount) {
+ getNativeCanvas().restoreToCount(saveCount);
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected void release() {
+ nativeObjectRegistry.unregister(getNativeId());
+ canvasReflector.release();
+ }
+
+ @Implementation(maxSdk = KITKAT_WATCH)
+ protected static int initRaster(int bitmapHandle) {
+ return (int) nativeObjectRegistry.register(new NativeCanvas());
+ }
+
+ @Implementation(minSdk = LOLLIPOP, maxSdk = LOLLIPOP_MR1)
+ protected static long initRaster(long bitmapHandle) {
+ return nativeObjectRegistry.register(new NativeCanvas());
+ }
+
+ @Implementation(minSdk = M, maxSdk = N_MR1)
+ protected static long initRaster(Bitmap bitmap) {
+ return nativeObjectRegistry.register(new NativeCanvas());
+ }
+
+ @Implementation(minSdk = O, maxSdk = P)
+ protected static long nInitRaster(Bitmap bitmap) {
+ return nativeObjectRegistry.register(new NativeCanvas());
+ }
+
+ @Implementation(minSdk = Q)
+ protected static long nInitRaster(long bitmapHandle) {
+ return nativeObjectRegistry.register(new NativeCanvas());
+ }
+
+ @Implementation(minSdk = O)
+ protected static int nGetSaveCount(long canvasHandle) {
+ return nativeObjectRegistry.getNativeObject(canvasHandle).getSaveCount();
+ }
+
+ @Implementation(minSdk = O)
+ protected static int nSave(long canvasHandle, int saveFlags) {
+ return nativeObjectRegistry.getNativeObject(canvasHandle).save();
+ }
+
+ @Implementation(maxSdk = KITKAT_WATCH)
+ protected static int native_saveLayer(int nativeCanvas, RectF bounds, int paint, int layerFlags) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(maxSdk = KITKAT_WATCH)
+ protected static int native_saveLayer(
+ int nativeCanvas, float l, float t, float r, float b, int paint, int layerFlags) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
+ protected static int native_saveLayer(
+ long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(minSdk = O, maxSdk = R)
+ protected static int nSaveLayer(
+ long nativeCanvas, float l, float t, float r, float b, long nativePaint, int layerFlags) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(minSdk = S)
+ protected static int nSaveLayer(
+ long nativeCanvas, float l, float t, float r, float b, long nativePaint) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(maxSdk = KITKAT_WATCH)
+ protected static int native_saveLayerAlpha(
+ int nativeCanvas, RectF bounds, int alpha, int layerFlags) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(maxSdk = KITKAT_WATCH)
+ protected static int native_saveLayerAlpha(
+ int nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
+ protected static int native_saveLayerAlpha(
+ long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(minSdk = O, maxSdk = R)
+ protected static int nSaveLayerAlpha(
+ long nativeCanvas, float l, float t, float r, float b, int alpha, int layerFlags) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(minSdk = S)
+ protected static int nSaveLayerAlpha(
+ long nativeCanvas, float l, float t, float r, float b, int alpha) {
+ return nativeObjectRegistry.getNativeObject(nativeCanvas).save();
+ }
+
+ @Implementation(minSdk = O)
+ protected static boolean nRestore(long canvasHandle) {
+ return nativeObjectRegistry.getNativeObject(canvasHandle).restore();
+ }
+
+ @Implementation(minSdk = O)
+ protected static void nRestoreToCount(long canvasHandle, int saveCount) {
+ nativeObjectRegistry.getNativeObject(canvasHandle).restoreToCount(saveCount);
+ }
+
+ private static class PathPaintHistoryEvent {
+ private final Path drawnPath;
+ private final Paint pathPaint;
+
+ PathPaintHistoryEvent(Path drawnPath, Paint pathPaint) {
+ this.drawnPath = drawnPath;
+ this.pathPaint = pathPaint;
+ }
+ }
+
+ @Resetter
+ public static void reset() {
+ nativeObjectRegistry.clear();
+ }
+
+ @SuppressWarnings("MemberName")
+ @ForType(Canvas.class)
+ private interface CanvasReflector {
+ @Direct
+ void __constructor__(Bitmap bitmap);
+
+ @Direct
+ void release();
+ }
+
+ private static class NativeCanvas {
+ private int saveCount = 1;
+
+ int save() {
+ return saveCount++;
+ }
+
+ boolean restore() {
+ if (saveCount > 1) {
+ saveCount--;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ int getSaveCount() {
+ return saveCount;
+ }
+
+ void restoreToCount(int saveCount) {
+ if (saveCount > 0) {
+ this.saveCount = saveCount;
+ }
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java
new file mode 100644
index 000000000..a85af0c41
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyMatrix.java
@@ -0,0 +1,662 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+
+import android.graphics.Matrix;
+import android.graphics.Matrix.ScaleToFit;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import java.awt.geom.AffineTransform;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+
+@SuppressWarnings({"UnusedDeclaration"})
+@Implements(value = Matrix.class, isInAndroidSdk = false)
+public class ShadowLegacyMatrix extends ShadowMatrix {
+
+ private static final float EPSILON = 1e-3f;
+
+ private final Deque<String> preOps = new ArrayDeque<>();
+ private final Deque<String> postOps = new ArrayDeque<>();
+ private final Map<String, String> setOps = new LinkedHashMap<>();
+
+ private SimpleMatrix simpleMatrix = SimpleMatrix.newIdentityMatrix();
+
+ @Implementation
+ protected void __constructor__(Matrix src) {
+ set(src);
+ }
+
+ /**
+ * A list of all 'pre' operations performed on this Matrix. The last operation performed will be
+ * first in the list.
+ *
+ * @return A list of all 'pre' operations performed on this Matrix.
+ */
+ @Override
+ public List<String> getPreOperations() {
+ return Collections.unmodifiableList(new ArrayList<>(preOps));
+ }
+
+ /**
+ * A list of all 'post' operations performed on this Matrix. The last operation performed will be
+ * last in the list.
+ *
+ * @return A list of all 'post' operations performed on this Matrix.
+ */
+ @Override
+ public List<String> getPostOperations() {
+ return Collections.unmodifiableList(new ArrayList<>(postOps));
+ }
+
+ /**
+ * A map of all 'set' operations performed on this Matrix.
+ *
+ * @return A map of all 'set' operations performed on this Matrix.
+ */
+ @Override
+ public Map<String, String> getSetOperations() {
+ return Collections.unmodifiableMap(new LinkedHashMap<>(setOps));
+ }
+
+ @Implementation
+ protected boolean isIdentity() {
+ return simpleMatrix.equals(SimpleMatrix.IDENTITY);
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ protected boolean isAffine() {
+ return simpleMatrix.isAffine();
+ }
+
+ @Implementation
+ protected boolean rectStaysRect() {
+ return simpleMatrix.rectStaysRect();
+ }
+
+ @Implementation
+ protected void getValues(float[] values) {
+ simpleMatrix.getValues(values);
+ }
+
+ @Implementation
+ protected void setValues(float[] values) {
+ simpleMatrix = new SimpleMatrix(values);
+ }
+
+ @Implementation
+ protected void set(Matrix src) {
+ reset();
+ if (src != null) {
+ ShadowLegacyMatrix shadowMatrix = Shadow.extract(src);
+ preOps.addAll(shadowMatrix.preOps);
+ postOps.addAll(shadowMatrix.postOps);
+ setOps.putAll(shadowMatrix.setOps);
+ simpleMatrix = new SimpleMatrix(getSimpleMatrix(src));
+ }
+ }
+
+ @Implementation
+ protected void reset() {
+ preOps.clear();
+ postOps.clear();
+ setOps.clear();
+ simpleMatrix = SimpleMatrix.newIdentityMatrix();
+ }
+
+ @Implementation
+ protected void setTranslate(float dx, float dy) {
+ setOps.put(TRANSLATE, dx + " " + dy);
+ simpleMatrix = SimpleMatrix.translate(dx, dy);
+ }
+
+ @Implementation
+ protected void setScale(float sx, float sy, float px, float py) {
+ setOps.put(SCALE, sx + " " + sy + " " + px + " " + py);
+ simpleMatrix = SimpleMatrix.scale(sx, sy, px, py);
+ }
+
+ @Implementation
+ protected void setScale(float sx, float sy) {
+ setOps.put(SCALE, sx + " " + sy);
+ simpleMatrix = SimpleMatrix.scale(sx, sy);
+ }
+
+ @Implementation
+ protected void setRotate(float degrees, float px, float py) {
+ setOps.put(ROTATE, degrees + " " + px + " " + py);
+ simpleMatrix = SimpleMatrix.rotate(degrees, px, py);
+ }
+
+ @Implementation
+ protected void setRotate(float degrees) {
+ setOps.put(ROTATE, Float.toString(degrees));
+ simpleMatrix = SimpleMatrix.rotate(degrees);
+ }
+
+ @Implementation
+ protected void setSinCos(float sinValue, float cosValue, float px, float py) {
+ setOps.put(SINCOS, sinValue + " " + cosValue + " " + px + " " + py);
+ simpleMatrix = SimpleMatrix.sinCos(sinValue, cosValue, px, py);
+ }
+
+ @Implementation
+ protected void setSinCos(float sinValue, float cosValue) {
+ setOps.put(SINCOS, sinValue + " " + cosValue);
+ simpleMatrix = SimpleMatrix.sinCos(sinValue, cosValue);
+ }
+
+ @Implementation
+ protected void setSkew(float kx, float ky, float px, float py) {
+ setOps.put(SKEW, kx + " " + ky + " " + px + " " + py);
+ simpleMatrix = SimpleMatrix.skew(kx, ky, px, py);
+ }
+
+ @Implementation
+ protected void setSkew(float kx, float ky) {
+ setOps.put(SKEW, kx + " " + ky);
+ simpleMatrix = SimpleMatrix.skew(kx, ky);
+ }
+
+ @Implementation
+ protected boolean setConcat(Matrix a, Matrix b) {
+ simpleMatrix = getSimpleMatrix(a).multiply(getSimpleMatrix(b));
+ return true;
+ }
+
+ @Implementation
+ protected boolean preTranslate(float dx, float dy) {
+ preOps.addFirst(TRANSLATE + " " + dx + " " + dy);
+ return preConcat(SimpleMatrix.translate(dx, dy));
+ }
+
+ @Implementation
+ protected boolean preScale(float sx, float sy, float px, float py) {
+ preOps.addFirst(SCALE + " " + sx + " " + sy + " " + px + " " + py);
+ return preConcat(SimpleMatrix.scale(sx, sy, px, py));
+ }
+
+ @Implementation
+ protected boolean preScale(float sx, float sy) {
+ preOps.addFirst(SCALE + " " + sx + " " + sy);
+ return preConcat(SimpleMatrix.scale(sx, sy));
+ }
+
+ @Implementation
+ protected boolean preRotate(float degrees, float px, float py) {
+ preOps.addFirst(ROTATE + " " + degrees + " " + px + " " + py);
+ return preConcat(SimpleMatrix.rotate(degrees, px, py));
+ }
+
+ @Implementation
+ protected boolean preRotate(float degrees) {
+ preOps.addFirst(ROTATE + " " + Float.toString(degrees));
+ return preConcat(SimpleMatrix.rotate(degrees));
+ }
+
+ @Implementation
+ protected boolean preSkew(float kx, float ky, float px, float py) {
+ preOps.addFirst(SKEW + " " + kx + " " + ky + " " + px + " " + py);
+ return preConcat(SimpleMatrix.skew(kx, ky, px, py));
+ }
+
+ @Implementation
+ protected boolean preSkew(float kx, float ky) {
+ preOps.addFirst(SKEW + " " + kx + " " + ky);
+ return preConcat(SimpleMatrix.skew(kx, ky));
+ }
+
+ @Implementation
+ protected boolean preConcat(Matrix other) {
+ preOps.addFirst(MATRIX + " " + other);
+ return preConcat(getSimpleMatrix(other));
+ }
+
+ @Implementation
+ protected boolean postTranslate(float dx, float dy) {
+ postOps.addLast(TRANSLATE + " " + dx + " " + dy);
+ return postConcat(SimpleMatrix.translate(dx, dy));
+ }
+
+ @Implementation
+ protected boolean postScale(float sx, float sy, float px, float py) {
+ postOps.addLast(SCALE + " " + sx + " " + sy + " " + px + " " + py);
+ return postConcat(SimpleMatrix.scale(sx, sy, px, py));
+ }
+
+ @Implementation
+ protected boolean postScale(float sx, float sy) {
+ postOps.addLast(SCALE + " " + sx + " " + sy);
+ return postConcat(SimpleMatrix.scale(sx, sy));
+ }
+
+ @Implementation
+ protected boolean postRotate(float degrees, float px, float py) {
+ postOps.addLast(ROTATE + " " + degrees + " " + px + " " + py);
+ return postConcat(SimpleMatrix.rotate(degrees, px, py));
+ }
+
+ @Implementation
+ protected boolean postRotate(float degrees) {
+ postOps.addLast(ROTATE + " " + Float.toString(degrees));
+ return postConcat(SimpleMatrix.rotate(degrees));
+ }
+
+ @Implementation
+ protected boolean postSkew(float kx, float ky, float px, float py) {
+ postOps.addLast(SKEW + " " + kx + " " + ky + " " + px + " " + py);
+ return postConcat(SimpleMatrix.skew(kx, ky, px, py));
+ }
+
+ @Implementation
+ protected boolean postSkew(float kx, float ky) {
+ postOps.addLast(SKEW + " " + kx + " " + ky);
+ return postConcat(SimpleMatrix.skew(kx, ky));
+ }
+
+ @Implementation
+ protected boolean postConcat(Matrix other) {
+ postOps.addLast(MATRIX + " " + other);
+ return postConcat(getSimpleMatrix(other));
+ }
+
+ @Implementation
+ protected boolean invert(Matrix inverse) {
+ final SimpleMatrix inverseMatrix = simpleMatrix.invert();
+ if (inverseMatrix != null) {
+ if (inverse != null) {
+ final ShadowLegacyMatrix shadowInverse = Shadow.extract(inverse);
+ shadowInverse.simpleMatrix = inverseMatrix;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ boolean hasPerspective() {
+ return (simpleMatrix.mValues[6] != 0 || simpleMatrix.mValues[7] != 0 || simpleMatrix.mValues[8] != 1);
+ }
+
+ protected AffineTransform getAffineTransform() {
+ // the AffineTransform constructor takes the value in a different order
+ // for a matrix [ 0 1 2 ]
+ // [ 3 4 5 ]
+ // the order is 0, 3, 1, 4, 2, 5...
+ return new AffineTransform(
+ simpleMatrix.mValues[0],
+ simpleMatrix.mValues[3],
+ simpleMatrix.mValues[1],
+ simpleMatrix.mValues[4],
+ simpleMatrix.mValues[2],
+ simpleMatrix.mValues[5]);
+ }
+
+ public PointF mapPoint(float x, float y) {
+ return simpleMatrix.transform(new PointF(x, y));
+ }
+
+ public PointF mapPoint(PointF point) {
+ return simpleMatrix.transform(point);
+ }
+
+ @Implementation
+ protected boolean mapRect(RectF destination, RectF source) {
+ final PointF leftTop = mapPoint(source.left, source.top);
+ final PointF rightBottom = mapPoint(source.right, source.bottom);
+ destination.set(
+ Math.min(leftTop.x, rightBottom.x),
+ Math.min(leftTop.y, rightBottom.y),
+ Math.max(leftTop.x, rightBottom.x),
+ Math.max(leftTop.y, rightBottom.y));
+ return true;
+ }
+
+ @Implementation
+ protected void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex, int pointCount) {
+ for (int i = 0; i < pointCount; i++) {
+ final PointF mapped = mapPoint(src[srcIndex + i * 2], src[srcIndex + i * 2 + 1]);
+ dst[dstIndex + i * 2] = mapped.x;
+ dst[dstIndex + i * 2 + 1] = mapped.y;
+ }
+ }
+
+ @Implementation
+ protected void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex, int vectorCount) {
+ final float transX = simpleMatrix.mValues[Matrix.MTRANS_X];
+ final float transY = simpleMatrix.mValues[Matrix.MTRANS_Y];
+
+ simpleMatrix.mValues[Matrix.MTRANS_X] = 0;
+ simpleMatrix.mValues[Matrix.MTRANS_Y] = 0;
+
+ for (int i = 0; i < vectorCount; i++) {
+ final PointF mapped = mapPoint(src[srcIndex + i * 2], src[srcIndex + i * 2 + 1]);
+ dst[dstIndex + i * 2] = mapped.x;
+ dst[dstIndex + i * 2 + 1] = mapped.y;
+ }
+
+ simpleMatrix.mValues[Matrix.MTRANS_X] = transX;
+ simpleMatrix.mValues[Matrix.MTRANS_Y] = transY;
+ }
+
+ @Implementation
+ protected float mapRadius(float radius) {
+ float[] src = new float[] {radius, 0.f, 0.f, radius};
+ mapVectors(src, 0, src, 0, 2);
+
+ float l1 = (float) Math.hypot(src[0], src[1]);
+ float l2 = (float) Math.hypot(src[2], src[3]);
+ return (float) Math.sqrt(l1 * l2);
+ }
+
+ @Implementation
+ protected boolean setRectToRect(RectF src, RectF dst, Matrix.ScaleToFit stf) {
+ if (src.isEmpty()) {
+ reset();
+ return false;
+ }
+ return simpleMatrix.setRectToRect(src, dst, stf);
+ }
+
+ @Implementation
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Matrix) {
+ return getSimpleMatrix(((Matrix) obj)).equals(simpleMatrix);
+ } else {
+ return obj instanceof ShadowMatrix && obj.equals(simpleMatrix);
+ }
+ }
+
+ @Implementation(minSdk = KITKAT)
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(simpleMatrix);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Matrix[pre=" + preOps + ", set=" + setOps + ", post=" + postOps + "]";
+ }
+
+ private static SimpleMatrix getSimpleMatrix(Matrix matrix) {
+ final ShadowLegacyMatrix otherMatrix = Shadow.extract(matrix);
+ return otherMatrix.simpleMatrix;
+ }
+
+ private boolean postConcat(SimpleMatrix matrix) {
+ simpleMatrix = matrix.multiply(simpleMatrix);
+ return true;
+ }
+
+ private boolean preConcat(SimpleMatrix matrix) {
+ simpleMatrix = simpleMatrix.multiply(matrix);
+ return true;
+ }
+
+ /**
+ * A simple implementation of an immutable matrix.
+ */
+ private static class SimpleMatrix {
+ private static final SimpleMatrix IDENTITY = newIdentityMatrix();
+
+ private static SimpleMatrix newIdentityMatrix() {
+ return new SimpleMatrix(
+ new float[] {
+ 1.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f,
+ });
+ }
+
+ private final float[] mValues;
+
+ SimpleMatrix(SimpleMatrix matrix) {
+ mValues = Arrays.copyOf(matrix.mValues, matrix.mValues.length);
+ }
+
+ private SimpleMatrix(float[] values) {
+ if (values.length != 9) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ mValues = Arrays.copyOf(values, 9);
+ }
+
+ public boolean isAffine() {
+ return mValues[6] == 0.0f && mValues[7] == 0.0f && mValues[8] == 1.0f;
+ }
+
+ public boolean rectStaysRect() {
+ final float m00 = mValues[0];
+ final float m01 = mValues[1];
+ final float m10 = mValues[3];
+ final float m11 = mValues[4];
+ return (m00 == 0 && m11 == 0 && m01 != 0 && m10 != 0) || (m00 != 0 && m11 != 0 && m01 == 0 && m10 == 0);
+ }
+
+ public void getValues(float[] values) {
+ if (values.length < 9) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ System.arraycopy(mValues, 0, values, 0, 9);
+ }
+
+ public static SimpleMatrix translate(float dx, float dy) {
+ return new SimpleMatrix(new float[] {
+ 1.0f, 0.0f, dx,
+ 0.0f, 1.0f, dy,
+ 0.0f, 0.0f, 1.0f,
+ });
+ }
+
+ public static SimpleMatrix scale(float sx, float sy, float px, float py) {
+ return new SimpleMatrix(new float[] {
+ sx, 0.0f, px * (1 - sx),
+ 0.0f, sy, py * (1 - sy),
+ 0.0f, 0.0f, 1.0f,
+ });
+ }
+
+ public static SimpleMatrix scale(float sx, float sy) {
+ return new SimpleMatrix(new float[] {
+ sx, 0.0f, 0.0f,
+ 0.0f, sy, 0.0f,
+ 0.0f, 0.0f, 1.0f,
+ });
+ }
+
+ public static SimpleMatrix rotate(float degrees, float px, float py) {
+ final double radians = Math.toRadians(degrees);
+ final float sin = (float) Math.sin(radians);
+ final float cos = (float) Math.cos(radians);
+ return sinCos(sin, cos, px, py);
+ }
+
+ public static SimpleMatrix rotate(float degrees) {
+ final double radians = Math.toRadians(degrees);
+ final float sin = (float) Math.sin(radians);
+ final float cos = (float) Math.cos(radians);
+ return sinCos(sin, cos);
+ }
+
+ public static SimpleMatrix sinCos(float sin, float cos, float px, float py) {
+ return new SimpleMatrix(new float[] {
+ cos, -sin, sin * py + (1 - cos) * px,
+ sin, cos, -sin * px + (1 - cos) * py,
+ 0.0f, 0.0f, 1.0f,
+ });
+ }
+
+ public static SimpleMatrix sinCos(float sin, float cos) {
+ return new SimpleMatrix(new float[] {
+ cos, -sin, 0.0f,
+ sin, cos, 0.0f,
+ 0.0f, 0.0f, 1.0f,
+ });
+ }
+
+ public static SimpleMatrix skew(float kx, float ky, float px, float py) {
+ return new SimpleMatrix(new float[] {
+ 1.0f, kx, -kx * py,
+ ky, 1.0f, -ky * px,
+ 0.0f, 0.0f, 1.0f,
+ });
+ }
+
+ public static SimpleMatrix skew(float kx, float ky) {
+ return new SimpleMatrix(new float[] {
+ 1.0f, kx, 0.0f,
+ ky, 1.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f,
+ });
+ }
+
+ public SimpleMatrix multiply(SimpleMatrix matrix) {
+ final float[] values = new float[9];
+ for (int i = 0; i < values.length; ++i) {
+ final int row = i / 3;
+ final int col = i % 3;
+ for (int j = 0; j < 3; ++j) {
+ values[i] += mValues[row * 3 + j] * matrix.mValues[j * 3 + col];
+ }
+ }
+ return new SimpleMatrix(values);
+ }
+
+ public SimpleMatrix invert() {
+ final float invDet = inverseDeterminant();
+ if (invDet == 0) {
+ return null;
+ }
+
+ final float[] src = mValues;
+ final float[] dst = new float[9];
+ dst[0] = cross_scale(src[4], src[8], src[5], src[7], invDet);
+ dst[1] = cross_scale(src[2], src[7], src[1], src[8], invDet);
+ dst[2] = cross_scale(src[1], src[5], src[2], src[4], invDet);
+
+ dst[3] = cross_scale(src[5], src[6], src[3], src[8], invDet);
+ dst[4] = cross_scale(src[0], src[8], src[2], src[6], invDet);
+ dst[5] = cross_scale(src[2], src[3], src[0], src[5], invDet);
+
+ dst[6] = cross_scale(src[3], src[7], src[4], src[6], invDet);
+ dst[7] = cross_scale(src[1], src[6], src[0], src[7], invDet);
+ dst[8] = cross_scale(src[0], src[4], src[1], src[3], invDet);
+ return new SimpleMatrix(dst);
+ }
+
+ public PointF transform(PointF point) {
+ return new PointF(
+ point.x * mValues[0] + point.y * mValues[1] + mValues[2],
+ point.x * mValues[3] + point.y * mValues[4] + mValues[5]);
+ }
+
+ // See: https://android.googlesource.com/platform/frameworks/base/+/6fca81de9b2079ec88e785f58bf49bf1f0c105e2/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java
+ protected boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
+ if (dst.isEmpty()) {
+ mValues[0] =
+ mValues[1] =
+ mValues[2] = mValues[3] = mValues[4] = mValues[5] = mValues[6] = mValues[7] = 0;
+ mValues[8] = 1;
+ } else {
+ float tx = dst.width() / src.width();
+ float sx = dst.width() / src.width();
+ float ty = dst.height() / src.height();
+ float sy = dst.height() / src.height();
+ boolean xLarger = false;
+
+ if (stf != ScaleToFit.FILL) {
+ if (sx > sy) {
+ xLarger = true;
+ sx = sy;
+ } else {
+ sy = sx;
+ }
+ }
+
+ tx = dst.left - src.left * sx;
+ ty = dst.top - src.top * sy;
+ if (stf == ScaleToFit.CENTER || stf == ScaleToFit.END) {
+ float diff;
+
+ if (xLarger) {
+ diff = dst.width() - src.width() * sy;
+ } else {
+ diff = dst.height() - src.height() * sy;
+ }
+
+ if (stf == ScaleToFit.CENTER) {
+ diff = diff / 2;
+ }
+
+ if (xLarger) {
+ tx += diff;
+ } else {
+ ty += diff;
+ }
+ }
+
+ mValues[0] = sx;
+ mValues[4] = sy;
+ mValues[2] = tx;
+ mValues[5] = ty;
+ mValues[1] = mValues[3] = mValues[6] = mValues[7] = 0;
+ }
+ // shared cleanup
+ mValues[8] = 1;
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return this == o || (o instanceof SimpleMatrix && equals((SimpleMatrix) o));
+ }
+
+ @SuppressWarnings("NonOverridingEquals")
+ public boolean equals(SimpleMatrix matrix) {
+ if (matrix == null) {
+ return false;
+ }
+ for (int i = 0; i < mValues.length; i++) {
+ if (!isNearlyZero(matrix.mValues[i] - mValues[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(mValues);
+ }
+
+ private static boolean isNearlyZero(float value) {
+ return Math.abs(value) < EPSILON;
+ }
+
+ private static float cross(float a, float b, float c, float d) {
+ return a * b - c * d;
+ }
+
+ private static float cross_scale(float a, float b, float c, float d, float scale) {
+ return cross(a, b, c, d) * scale;
+ }
+
+ private float inverseDeterminant() {
+ final float determinant = mValues[0] * cross(mValues[4], mValues[8], mValues[5], mValues[7]) +
+ mValues[1] * cross(mValues[5], mValues[6], mValues[3], mValues[8]) +
+ mValues[2] * cross(mValues[3], mValues[7], mValues[4], mValues[6]);
+ return isNearlyZero(determinant) ? 0.0f : 1.0f / determinant;
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyPath.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyPath.java
new file mode 100644
index 000000000..b4f113a4a
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyPath.java
@@ -0,0 +1,558 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN;
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static org.robolectric.shadow.api.Shadow.extract;
+import static org.robolectric.shadows.ShadowPath.Point.Type.LINE_TO;
+import static org.robolectric.shadows.ShadowPath.Point.Type.MOVE_TO;
+
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.RectF;
+import android.util.Log;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Area;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.RoundRectangle2D;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+/** The shadow only supports straight-line paths. */
+@SuppressWarnings({"UnusedDeclaration"})
+@Implements(value = Path.class, isInAndroidSdk = false)
+public class ShadowLegacyPath extends ShadowPath {
+ private static final String TAG = ShadowLegacyPath.class.getSimpleName();
+ private static final float EPSILON = 1e-4f;
+
+ @RealObject private Path realObject;
+
+ private List<Point> points = new ArrayList<>();
+
+ private float mLastX = 0;
+ private float mLastY = 0;
+ private Path2D mPath = new Path2D.Double();
+ private boolean mCachedIsEmpty = true;
+ private Path.FillType mFillType = Path.FillType.WINDING;
+ protected boolean isSimplePath;
+
+ @Implementation
+ protected void __constructor__(Path path) {
+ ShadowLegacyPath shadowPath = extract(path);
+ points = new ArrayList<>(shadowPath.getPoints());
+ mPath.append(shadowPath.mPath, /*connect=*/ false);
+ mFillType = shadowPath.getFillType();
+ }
+
+ Path2D getJavaShape() {
+ return mPath;
+ }
+
+ @Implementation
+ protected void moveTo(float x, float y) {
+ mPath.moveTo(mLastX = x, mLastY = y);
+
+ // Legacy recording behavior
+ Point p = new Point(x, y, MOVE_TO);
+ points.add(p);
+ }
+
+ @Implementation
+ protected void lineTo(float x, float y) {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+ mPath.lineTo(mLastX = x, mLastY = y);
+
+ // Legacy recording behavior
+ Point point = new Point(x, y, LINE_TO);
+ points.add(point);
+ }
+
+ @Implementation
+ protected void quadTo(float x1, float y1, float x2, float y2) {
+ isSimplePath = false;
+ if (!hasPoints()) {
+ moveTo(0, 0);
+ }
+ mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
+ }
+
+ @Implementation
+ protected void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
+ if (!hasPoints()) {
+ mPath.moveTo(0, 0);
+ }
+ mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
+ }
+
+ private boolean hasPoints() {
+ return !mPath.getPathIterator(null).isDone();
+ }
+
+ @Implementation
+ protected void reset() {
+ mPath.reset();
+ mLastX = 0;
+ mLastY = 0;
+
+ // Legacy recording behavior
+ points.clear();
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ protected float[] approximate(float acceptableError) {
+ PathIterator iterator = mPath.getPathIterator(null, acceptableError);
+
+ float segment[] = new float[6];
+ float totalLength = 0;
+ ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>();
+ Point2D.Float previousPoint = null;
+ while (!iterator.isDone()) {
+ int type = iterator.currentSegment(segment);
+ Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]);
+ // MoveTo shouldn't affect the length
+ if (previousPoint != null && type != PathIterator.SEG_MOVETO) {
+ totalLength += (float) currentPoint.distance(previousPoint);
+ }
+ previousPoint = currentPoint;
+ points.add(currentPoint);
+ iterator.next();
+ }
+
+ int nPoints = points.size();
+ float[] result = new float[nPoints * 3];
+ previousPoint = null;
+ // Distance that we've covered so far. Used to calculate the fraction of the path that
+ // we've covered up to this point.
+ float walkedDistance = .0f;
+ for (int i = 0; i < nPoints; i++) {
+ Point2D.Float point = points.get(i);
+ float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
+ walkedDistance += distance;
+ result[i * 3] = walkedDistance / totalLength;
+ result[i * 3 + 1] = point.x;
+ result[i * 3 + 2] = point.y;
+
+ previousPoint = point;
+ }
+
+ return result;
+ }
+
+ /**
+ * @return all the points that have been added to the {@code Path}
+ */
+ @Override
+ public List<Point> getPoints() {
+ return points;
+ }
+
+ @Implementation
+ protected void rewind() {
+ // call out to reset since there's nothing to optimize in
+ // terms of data structs.
+ reset();
+ }
+
+ @Implementation
+ protected void set(Path src) {
+ mPath.reset();
+
+ ShadowLegacyPath shadowSrc = extract(src);
+ setFillType(shadowSrc.mFillType);
+ mPath.append(shadowSrc.mPath, false /*connect*/);
+ }
+
+ @Implementation(minSdk = KITKAT)
+ protected boolean op(Path path1, Path path2, Path.Op op) {
+ Log.w(TAG, "android.graphics.Path#op() not supported yet.");
+ return false;
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ protected boolean isConvex() {
+ Log.w(TAG, "android.graphics.Path#isConvex() not supported yet.");
+ return true;
+ }
+
+ @Implementation
+ protected Path.FillType getFillType() {
+ return mFillType;
+ }
+
+ @Implementation
+ protected void setFillType(Path.FillType fillType) {
+ mFillType = fillType;
+ mPath.setWindingRule(getWindingRule(fillType));
+ }
+
+ /**
+ * Returns the Java2D winding rules matching a given Android {@link
+ * android.graphics.Path.FillType}.
+ *
+ * @param type the android fill type
+ * @return the matching java2d winding rule.
+ */
+ private static int getWindingRule(Path.FillType type) {
+ switch (type) {
+ case WINDING:
+ case INVERSE_WINDING:
+ return GeneralPath.WIND_NON_ZERO;
+ case EVEN_ODD:
+ case INVERSE_EVEN_ODD:
+ return GeneralPath.WIND_EVEN_ODD;
+
+ default:
+ assert false;
+ return GeneralPath.WIND_NON_ZERO;
+ }
+ }
+
+ @Implementation
+ protected boolean isInverseFillType() {
+ throw new UnsupportedOperationException("isInverseFillType");
+ }
+
+ @Implementation
+ protected void toggleInverseFillType() {
+ throw new UnsupportedOperationException("toggleInverseFillType");
+ }
+
+ @Implementation
+ protected boolean isEmpty() {
+ if (!mCachedIsEmpty) {
+ return false;
+ }
+
+ mCachedIsEmpty = Boolean.TRUE;
+ for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
+ // int type = it.currentSegment(coords);
+ // if (type != PathIterator.SEG_MOVETO) {
+ // Once we know that the path is not empty, we do not need to check again unless
+ // Path#reset is called.
+ mCachedIsEmpty = false;
+ return false;
+ // }
+ }
+
+ return true;
+ }
+
+ @Implementation
+ protected boolean isRect(RectF rect) {
+ // create an Area that can test if the path is a rect
+ Area area = new Area(mPath);
+ if (area.isRectangular()) {
+ if (rect != null) {
+ fillBounds(rect);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Implementation
+ protected void computeBounds(RectF bounds, boolean exact) {
+ fillBounds(bounds);
+ }
+
+ @Implementation
+ protected void incReserve(int extraPtCount) {
+ throw new UnsupportedOperationException("incReserve");
+ }
+
+ @Implementation
+ protected void rMoveTo(float dx, float dy) {
+ dx += mLastX;
+ dy += mLastY;
+ mPath.moveTo(mLastX = dx, mLastY = dy);
+ }
+
+ @Implementation
+ protected void rLineTo(float dx, float dy) {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+
+ if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
+ // The delta is so small that this shouldn't generate a line
+ return;
+ }
+
+ dx += mLastX;
+ dy += mLastY;
+ mPath.lineTo(mLastX = dx, mLastY = dy);
+ }
+
+ @Implementation
+ protected void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+ dx1 += mLastX;
+ dy1 += mLastY;
+ dx2 += mLastX;
+ dy2 += mLastY;
+ mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
+ }
+
+ @Implementation
+ protected void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+ x1 += mLastX;
+ y1 += mLastY;
+ x2 += mLastX;
+ y2 += mLastY;
+ x3 += mLastX;
+ y3 += mLastY;
+ mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
+ }
+
+ @Implementation
+ protected void arcTo(RectF oval, float startAngle, float sweepAngle) {
+ arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
+ }
+
+ @Implementation
+ protected void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) {
+ arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ protected void arcTo(
+ float left,
+ float top,
+ float right,
+ float bottom,
+ float startAngle,
+ float sweepAngle,
+ boolean forceMoveTo) {
+ isSimplePath = false;
+ Arc2D arc =
+ new Arc2D.Float(
+ left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN);
+ mPath.append(arc, true /*connect*/);
+ if (hasPoints()) {
+ resetLastPointFromPath();
+ }
+ }
+
+ @Implementation
+ protected void close() {
+ if (!hasPoints()) {
+ mPath.moveTo(mLastX = 0, mLastY = 0);
+ }
+ mPath.closePath();
+ }
+
+ @Implementation
+ protected void addRect(RectF rect, Direction dir) {
+ addRect(rect.left, rect.top, rect.right, rect.bottom, dir);
+ }
+
+ @Implementation
+ protected void addRect(float left, float top, float right, float bottom, Path.Direction dir) {
+ moveTo(left, top);
+
+ switch (dir) {
+ case CW:
+ lineTo(right, top);
+ lineTo(right, bottom);
+ lineTo(left, bottom);
+ break;
+ case CCW:
+ lineTo(left, bottom);
+ lineTo(right, bottom);
+ lineTo(right, top);
+ break;
+ }
+
+ close();
+
+ resetLastPointFromPath();
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ protected void addOval(float left, float top, float right, float bottom, Path.Direction dir) {
+ mPath.append(new Ellipse2D.Float(left, top, right - left, bottom - top), false);
+ }
+
+ @Implementation
+ protected void addCircle(float x, float y, float radius, Path.Direction dir) {
+ mPath.append(new Ellipse2D.Float(x - radius, y - radius, radius * 2, radius * 2), false);
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ protected void addArc(
+ float left, float top, float right, float bottom, float startAngle, float sweepAngle) {
+ mPath.append(
+ new Arc2D.Float(
+ left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN),
+ false);
+ }
+
+ @Implementation(minSdk = JELLY_BEAN)
+ protected void addRoundRect(RectF rect, float rx, float ry, Direction dir) {
+ addRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, dir);
+ }
+
+ @Implementation(minSdk = JELLY_BEAN)
+ protected void addRoundRect(RectF rect, float[] radii, Direction dir) {
+ if (rect == null) {
+ throw new NullPointerException("need rect parameter");
+ }
+ addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir);
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ protected void addRoundRect(
+ float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) {
+ mPath.append(
+ new RoundRectangle2D.Float(left, top, right - left, bottom - top, rx * 2, ry * 2), false);
+ }
+
+ @Implementation(minSdk = LOLLIPOP)
+ protected void addRoundRect(
+ float left, float top, float right, float bottom, float[] radii, Path.Direction dir) {
+ if (radii.length < 8) {
+ throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values");
+ }
+ isSimplePath = false;
+
+ float[] cornerDimensions = new float[radii.length];
+ for (int i = 0; i < radii.length; i++) {
+ cornerDimensions[i] = 2 * radii[i];
+ }
+ mPath.append(
+ new RoundRectangle(left, top, right - left, bottom - top, cornerDimensions), false);
+ }
+
+ @Implementation
+ protected void addPath(Path src, float dx, float dy) {
+ isSimplePath = false;
+ ShadowLegacyPath.addPath(realObject, src, AffineTransform.getTranslateInstance(dx, dy));
+ }
+
+ @Implementation
+ protected void addPath(Path src) {
+ isSimplePath = false;
+ ShadowLegacyPath.addPath(realObject, src, null);
+ }
+
+ @Implementation
+ protected void addPath(Path src, Matrix matrix) {
+ if (matrix == null) {
+ return;
+ }
+ ShadowLegacyPath shadowSrc = extract(src);
+ if (!shadowSrc.isSimplePath) isSimplePath = false;
+
+ ShadowLegacyMatrix shadowMatrix = extract(matrix);
+ ShadowLegacyPath.addPath(realObject, src, shadowMatrix.getAffineTransform());
+ }
+
+ private static void addPath(Path destPath, Path srcPath, AffineTransform transform) {
+ if (destPath == null) {
+ return;
+ }
+
+ if (srcPath == null) {
+ return;
+ }
+
+ ShadowLegacyPath shadowDestPath = extract(destPath);
+ ShadowLegacyPath shadowSrcPath = extract(srcPath);
+ if (transform != null) {
+ shadowDestPath.mPath.append(shadowSrcPath.mPath.getPathIterator(transform), false);
+ } else {
+ shadowDestPath.mPath.append(shadowSrcPath.mPath, false);
+ }
+ }
+
+ @Implementation
+ protected void offset(float dx, float dy, Path dst) {
+ if (dst != null) {
+ dst.set(realObject);
+ } else {
+ dst = realObject;
+ }
+ dst.offset(dx, dy);
+ }
+
+ @Implementation
+ protected void offset(float dx, float dy) {
+ GeneralPath newPath = new GeneralPath();
+
+ PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
+
+ newPath.append(iterator, false /*connect*/);
+ mPath = newPath;
+ }
+
+ @Implementation
+ protected void setLastPoint(float dx, float dy) {
+ mLastX = dx;
+ mLastY = dy;
+ }
+
+ @Implementation
+ protected void transform(Matrix matrix, Path dst) {
+ ShadowLegacyMatrix shadowMatrix = extract(matrix);
+
+ if (shadowMatrix.hasPerspective()) {
+ Log.w(TAG, "android.graphics.Path#transform() only supports affine transformations.");
+ }
+
+ GeneralPath newPath = new GeneralPath();
+
+ PathIterator iterator = mPath.getPathIterator(shadowMatrix.getAffineTransform());
+ newPath.append(iterator, false /*connect*/);
+
+ if (dst != null) {
+ ShadowLegacyPath shadowPath = extract(dst);
+ shadowPath.mPath = newPath;
+ } else {
+ mPath = newPath;
+ }
+ }
+
+ @Implementation
+ protected void transform(Matrix matrix) {
+ transform(matrix, null);
+ }
+
+ /**
+ * Fills the given {@link RectF} with the path bounds.
+ *
+ * @param bounds the RectF to be filled.
+ */
+ @Override
+ public void fillBounds(RectF bounds) {
+ Rectangle2D rect = mPath.getBounds2D();
+ bounds.left = (float) rect.getMinX();
+ bounds.right = (float) rect.getMaxX();
+ bounds.top = (float) rect.getMinY();
+ bounds.bottom = (float) rect.getMaxY();
+ }
+
+ private void resetLastPointFromPath() {
+ Point2D last = mPath.getCurrentPoint();
+ mLastX = (float) last.getX();
+ mLastY = (float) last.getY();
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java
new file mode 100644
index 000000000..378bbb03f
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyTypeface.java
@@ -0,0 +1,273 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.KITKAT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.O_MR1;
+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 org.robolectric.RuntimeEnvironment.getApiLevel;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.annotation.SuppressLint;
+import android.content.res.AssetManager;
+import android.graphics.FontFamily;
+import android.graphics.Typeface;
+import android.util.ArrayMap;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.HiddenApi;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.res.Fs;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Shadow for {@link Typeface}. */
+@Implements(value = Typeface.class, looseSignatures = true, isInAndroidSdk = false)
+@SuppressLint("NewApi")
+public class ShadowLegacyTypeface extends ShadowTypeface {
+ private static final Map<Long, FontDesc> FONTS = Collections.synchronizedMap(new HashMap<>());
+ private static final AtomicLong nextFontId = new AtomicLong(1);
+ private FontDesc description;
+
+ @HiddenApi
+ @Implementation(maxSdk = KITKAT)
+ protected void __constructor__(int fontId) {
+ description = findById((long) fontId);
+ }
+
+ @HiddenApi
+ @Implementation(minSdk = LOLLIPOP)
+ protected void __constructor__(long fontId) {
+ description = findById(fontId);
+ }
+
+ @Implementation
+ protected static void __staticInitializer__() {
+ Shadow.directInitialize(Typeface.class);
+ if (RuntimeEnvironment.getApiLevel() > R) {
+ Typeface.loadPreinstalledSystemFontMap();
+ }
+ }
+
+ @Implementation(minSdk = P)
+ protected static Typeface create(Typeface family, int weight, boolean italic) {
+ if (family == null) {
+ return createUnderlyingTypeface(null, weight);
+ } else {
+ ShadowTypeface shadowTypeface = Shadow.extract(family);
+ return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), weight);
+ }
+ }
+
+ @Implementation
+ protected static Typeface create(String familyName, int style) {
+ return createUnderlyingTypeface(familyName, style);
+ }
+
+ @Implementation
+ protected static Typeface create(Typeface family, int style) {
+ if (family == null) {
+ return createUnderlyingTypeface(null, style);
+ } else {
+ ShadowTypeface shadowTypeface = Shadow.extract(family);
+ return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), style);
+ }
+ }
+
+ @Implementation
+ protected static Typeface createFromAsset(AssetManager mgr, String path) {
+ ShadowAssetManager shadowAssetManager = Shadow.extract(mgr);
+ Collection<Path> assetDirs = shadowAssetManager.getAllAssetDirs();
+ for (Path assetDir : assetDirs) {
+ Path assetFile = assetDir.resolve(path);
+ if (Files.exists(assetFile)) {
+ return createUnderlyingTypeface(path, Typeface.NORMAL);
+ }
+
+ // maybe path is e.g. "myFont", but we should match "myFont.ttf" too?
+ Path[] files;
+ try {
+ files = Fs.listFiles(assetDir, f -> f.getFileName().toString().startsWith(path));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ if (files.length != 0) {
+ return createUnderlyingTypeface(path, Typeface.NORMAL);
+ }
+ }
+
+ throw new RuntimeException("Font asset not found " + path);
+ }
+
+ @Implementation(minSdk = O, maxSdk = P)
+ protected static Typeface createFromResources(AssetManager mgr, String path, int cookie) {
+ return createUnderlyingTypeface(path, Typeface.NORMAL);
+ }
+
+ @Implementation(minSdk = O)
+ protected static Typeface createFromResources(
+ Object /* FamilyResourceEntry */ entry,
+ Object /* AssetManager */ mgr,
+ Object /* String */ path) {
+ return createUnderlyingTypeface((String) path, Typeface.NORMAL);
+ }
+
+ @Implementation
+ protected static Typeface createFromFile(File path) {
+ String familyName = path.toPath().getFileName().toString();
+ return createUnderlyingTypeface(familyName, Typeface.NORMAL);
+ }
+
+ @Implementation
+ protected static Typeface createFromFile(String path) {
+ return createFromFile(new File(path));
+ }
+
+ @Implementation
+ protected int getStyle() {
+ return description.getStyle();
+ }
+
+ @Override
+ @Implementation
+ public boolean equals(Object o) {
+ if (o instanceof Typeface) {
+ Typeface other = ((Typeface) o);
+ return Objects.equals(getFontDescription(), shadowOf(other).getFontDescription());
+ }
+ return false;
+ }
+
+ @Override
+ @Implementation
+ public int hashCode() {
+ return getFontDescription().hashCode();
+ }
+
+ @HiddenApi
+ @Implementation(minSdk = LOLLIPOP)
+ protected static Typeface createFromFamilies(Object /*FontFamily[]*/ families) {
+ return null;
+ }
+
+ @HiddenApi
+ @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
+ protected static Typeface createFromFamiliesWithDefault(Object /*FontFamily[]*/ families) {
+ return null;
+ }
+
+ @Implementation(minSdk = O, maxSdk = O_MR1)
+ protected static Typeface createFromFamiliesWithDefault(
+ Object /*FontFamily[]*/ families, Object /* int */ weight, Object /* int */ italic) {
+ return createUnderlyingTypeface("fake-font", Typeface.NORMAL);
+ }
+
+ @Implementation(minSdk = P)
+ protected static Typeface createFromFamiliesWithDefault(
+ Object /*FontFamily[]*/ families,
+ Object /* String */ fallbackName,
+ Object /* int */ weight,
+ Object /* int */ italic) {
+ return createUnderlyingTypeface((String) fallbackName, Typeface.NORMAL);
+ }
+
+ @Implementation(minSdk = P, maxSdk = P)
+ protected static void buildSystemFallback(
+ String xmlPath,
+ String fontDir,
+ ArrayMap<String, Typeface> fontMap,
+ ArrayMap<String, FontFamily[]> fallbackMap) {
+ fontMap.put("sans-serif", createUnderlyingTypeface("sans-serif", 0));
+ }
+
+ /** Avoid spurious error message about /system/etc/fonts.xml */
+ @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
+ protected static void init() {}
+
+ @HiddenApi
+ @Implementation(minSdk = Q, maxSdk = R)
+ protected static void initSystemDefaultTypefaces(
+ Object systemFontMap, Object fallbacks, Object aliases) {}
+
+ @Resetter
+ public static synchronized void reset() {
+ FONTS.clear();
+ }
+
+ protected static Typeface createUnderlyingTypeface(String familyName, int style) {
+ long thisFontId = nextFontId.getAndIncrement();
+ FONTS.put(thisFontId, new FontDesc(familyName, style));
+ if (getApiLevel() >= LOLLIPOP) {
+ return ReflectionHelpers.callConstructor(
+ Typeface.class, ClassParameter.from(long.class, thisFontId));
+ } else {
+ return ReflectionHelpers.callConstructor(
+ Typeface.class, ClassParameter.from(int.class, (int) thisFontId));
+ }
+ }
+
+ private static synchronized FontDesc findById(long fontId) {
+ if (FONTS.containsKey(fontId)) {
+ return FONTS.get(fontId);
+ }
+ throw new RuntimeException("Unknown font id: " + fontId);
+ }
+
+ @Implementation(minSdk = O, maxSdk = R)
+ protected static long nativeCreateFromArray(long[] familyArray, int weight, int italic) {
+ // TODO: implement this properly
+ long thisFontId = nextFontId.getAndIncrement();
+ FONTS.put(thisFontId, new FontDesc(null, weight));
+ return thisFontId;
+ }
+
+ /**
+ * Returns the font description.
+ *
+ * @return Font description.
+ */
+ @Override
+ public FontDesc getFontDescription() {
+ return description;
+ }
+
+ @Implementation(minSdk = S)
+ protected static void nativeForceSetStaticFinalField(String fieldname, Typeface typeface) {
+ ReflectionHelpers.setStaticField(Typeface.class, fieldname, typeface);
+ }
+
+ @Implementation(minSdk = S)
+ protected static long nativeCreateFromArray(
+ long[] familyArray, long fallbackTypeface, int weight, int italic) {
+ return ShadowLegacyTypeface.nativeCreateFromArray(familyArray, weight, italic);
+ }
+
+ /** Shadow for {@link Typeface.Builder} */
+ @Implements(value = Typeface.Builder.class, minSdk = Q)
+ public static class ShadowBuilder {
+ @RealObject Typeface.Builder realBuilder;
+
+ @Implementation
+ protected Typeface build() {
+ String path = ReflectionHelpers.getField(realBuilder, "mPath");
+ return createUnderlyingTypeface(path, Typeface.NORMAL);
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java
index ef26f9e43..b4898fb71 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMatrix.java
@@ -1,29 +1,16 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
import android.graphics.Matrix;
-import android.graphics.Matrix.ScaleToFit;
-import android.graphics.PointF;
-import android.graphics.RectF;
-import java.awt.geom.AffineTransform;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
-import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadow.api.ShadowPicker;
+import org.robolectric.shadows.ShadowMatrix.Picker;
@SuppressWarnings({"UnusedDeclaration"})
-@Implements(Matrix.class)
-public class ShadowMatrix {
+@Implements(value = Matrix.class, shadowPicker = Picker.class)
+public abstract class ShadowMatrix {
public static final String TRANSLATE = "translate";
public static final String SCALE = "scale";
public static final String ROTATE = "rotate";
@@ -31,631 +18,36 @@ public class ShadowMatrix {
public static final String SKEW = "skew";
public static final String MATRIX = "matrix";
- private static final float EPSILON = 1e-3f;
-
- private final Deque<String> preOps = new ArrayDeque<>();
- private final Deque<String> postOps = new ArrayDeque<>();
- private final Map<String, String> setOps = new LinkedHashMap<>();
-
- private SimpleMatrix simpleMatrix = SimpleMatrix.newIdentityMatrix();
-
- @Implementation
- protected void __constructor__(Matrix src) {
- set(src);
- }
-
/**
- * A list of all 'pre' operations performed on this Matrix. The last operation performed will
- * be first in the list.
+ * A list of all 'pre' operations performed on this Matrix. The last operation performed will be
+ * first in the list.
+ *
* @return A list of all 'pre' operations performed on this Matrix.
*/
- public List<String> getPreOperations() {
- return Collections.unmodifiableList(new ArrayList<>(preOps));
- }
+ public abstract List<String> getPreOperations();
/**
- * A list of all 'post' operations performed on this Matrix. The last operation performed will
- * be last in the list.
+ * A list of all 'post' operations performed on this Matrix. The last operation performed will be
+ * last in the list.
+ *
* @return A list of all 'post' operations performed on this Matrix.
*/
- public List<String> getPostOperations() {
- return Collections.unmodifiableList(new ArrayList<>(postOps));
- }
+ public abstract List<String> getPostOperations();
/**
* A map of all 'set' operations performed on this Matrix.
+ *
* @return A map of all 'set' operations performed on this Matrix.
*/
- public Map<String, String> getSetOperations() {
- return Collections.unmodifiableMap(new LinkedHashMap<>(setOps));
- }
-
- @Implementation
- protected boolean isIdentity() {
- return simpleMatrix.equals(SimpleMatrix.IDENTITY);
- }
-
- @Implementation(minSdk = LOLLIPOP)
- protected boolean isAffine() {
- return simpleMatrix.isAffine();
- }
-
- @Implementation
- protected boolean rectStaysRect() {
- return simpleMatrix.rectStaysRect();
- }
-
- @Implementation
- protected void getValues(float[] values) {
- simpleMatrix.getValues(values);
- }
-
- @Implementation
- protected void setValues(float[] values) {
- simpleMatrix = new SimpleMatrix(values);
- }
-
- @Implementation
- protected void set(Matrix src) {
- reset();
- if (src != null) {
- ShadowMatrix shadowMatrix = Shadow.extract(src);
- preOps.addAll(shadowMatrix.preOps);
- postOps.addAll(shadowMatrix.postOps);
- setOps.putAll(shadowMatrix.setOps);
- simpleMatrix = new SimpleMatrix(getSimpleMatrix(src));
- }
- }
-
- @Implementation
- protected void reset() {
- preOps.clear();
- postOps.clear();
- setOps.clear();
- simpleMatrix = SimpleMatrix.newIdentityMatrix();
- }
-
- @Implementation
- protected void setTranslate(float dx, float dy) {
- setOps.put(TRANSLATE, dx + " " + dy);
- simpleMatrix = SimpleMatrix.translate(dx, dy);
- }
-
- @Implementation
- protected void setScale(float sx, float sy, float px, float py) {
- setOps.put(SCALE, sx + " " + sy + " " + px + " " + py);
- simpleMatrix = SimpleMatrix.scale(sx, sy, px, py);
- }
-
- @Implementation
- protected void setScale(float sx, float sy) {
- setOps.put(SCALE, sx + " " + sy);
- simpleMatrix = SimpleMatrix.scale(sx, sy);
- }
-
- @Implementation
- protected void setRotate(float degrees, float px, float py) {
- setOps.put(ROTATE, degrees + " " + px + " " + py);
- simpleMatrix = SimpleMatrix.rotate(degrees, px, py);
- }
-
- @Implementation
- protected void setRotate(float degrees) {
- setOps.put(ROTATE, Float.toString(degrees));
- simpleMatrix = SimpleMatrix.rotate(degrees);
- }
-
- @Implementation
- protected void setSinCos(float sinValue, float cosValue, float px, float py) {
- setOps.put(SINCOS, sinValue + " " + cosValue + " " + px + " " + py);
- simpleMatrix = SimpleMatrix.sinCos(sinValue, cosValue, px, py);
- }
-
- @Implementation
- protected void setSinCos(float sinValue, float cosValue) {
- setOps.put(SINCOS, sinValue + " " + cosValue);
- simpleMatrix = SimpleMatrix.sinCos(sinValue, cosValue);
- }
-
- @Implementation
- protected void setSkew(float kx, float ky, float px, float py) {
- setOps.put(SKEW, kx + " " + ky + " " + px + " " + py);
- simpleMatrix = SimpleMatrix.skew(kx, ky, px, py);
- }
-
- @Implementation
- protected void setSkew(float kx, float ky) {
- setOps.put(SKEW, kx + " " + ky);
- simpleMatrix = SimpleMatrix.skew(kx, ky);
- }
-
- @Implementation
- protected boolean setConcat(Matrix a, Matrix b) {
- simpleMatrix = getSimpleMatrix(a).multiply(getSimpleMatrix(b));
- return true;
- }
-
- @Implementation
- protected boolean preTranslate(float dx, float dy) {
- preOps.addFirst(TRANSLATE + " " + dx + " " + dy);
- return preConcat(SimpleMatrix.translate(dx, dy));
- }
+ public abstract Map<String, String> getSetOperations();
- @Implementation
- protected boolean preScale(float sx, float sy, float px, float py) {
- preOps.addFirst(SCALE + " " + sx + " " + sy + " " + px + " " + py);
- return preConcat(SimpleMatrix.scale(sx, sy, px, py));
- }
-
- @Implementation
- protected boolean preScale(float sx, float sy) {
- preOps.addFirst(SCALE + " " + sx + " " + sy);
- return preConcat(SimpleMatrix.scale(sx, sy));
- }
-
- @Implementation
- protected boolean preRotate(float degrees, float px, float py) {
- preOps.addFirst(ROTATE + " " + degrees + " " + px + " " + py);
- return preConcat(SimpleMatrix.rotate(degrees, px, py));
- }
-
- @Implementation
- protected boolean preRotate(float degrees) {
- preOps.addFirst(ROTATE + " " + Float.toString(degrees));
- return preConcat(SimpleMatrix.rotate(degrees));
- }
-
- @Implementation
- protected boolean preSkew(float kx, float ky, float px, float py) {
- preOps.addFirst(SKEW + " " + kx + " " + ky + " " + px + " " + py);
- return preConcat(SimpleMatrix.skew(kx, ky, px, py));
- }
-
- @Implementation
- protected boolean preSkew(float kx, float ky) {
- preOps.addFirst(SKEW + " " + kx + " " + ky);
- return preConcat(SimpleMatrix.skew(kx, ky));
- }
-
- @Implementation
- protected boolean preConcat(Matrix other) {
- preOps.addFirst(MATRIX + " " + other);
- return preConcat(getSimpleMatrix(other));
- }
-
- @Implementation
- protected boolean postTranslate(float dx, float dy) {
- postOps.addLast(TRANSLATE + " " + dx + " " + dy);
- return postConcat(SimpleMatrix.translate(dx, dy));
- }
-
- @Implementation
- protected boolean postScale(float sx, float sy, float px, float py) {
- postOps.addLast(SCALE + " " + sx + " " + sy + " " + px + " " + py);
- return postConcat(SimpleMatrix.scale(sx, sy, px, py));
- }
-
- @Implementation
- protected boolean postScale(float sx, float sy) {
- postOps.addLast(SCALE + " " + sx + " " + sy);
- return postConcat(SimpleMatrix.scale(sx, sy));
- }
-
- @Implementation
- protected boolean postRotate(float degrees, float px, float py) {
- postOps.addLast(ROTATE + " " + degrees + " " + px + " " + py);
- return postConcat(SimpleMatrix.rotate(degrees, px, py));
- }
-
- @Implementation
- protected boolean postRotate(float degrees) {
- postOps.addLast(ROTATE + " " + Float.toString(degrees));
- return postConcat(SimpleMatrix.rotate(degrees));
- }
-
- @Implementation
- protected boolean postSkew(float kx, float ky, float px, float py) {
- postOps.addLast(SKEW + " " + kx + " " + ky + " " + px + " " + py);
- return postConcat(SimpleMatrix.skew(kx, ky, px, py));
- }
-
- @Implementation
- protected boolean postSkew(float kx, float ky) {
- postOps.addLast(SKEW + " " + kx + " " + ky);
- return postConcat(SimpleMatrix.skew(kx, ky));
- }
-
- @Implementation
- protected boolean postConcat(Matrix other) {
- postOps.addLast(MATRIX + " " + other);
- return postConcat(getSimpleMatrix(other));
- }
-
- @Implementation
- protected boolean invert(Matrix inverse) {
- final SimpleMatrix inverseMatrix = simpleMatrix.invert();
- if (inverseMatrix != null) {
- if (inverse != null) {
- final ShadowMatrix shadowInverse = Shadow.extract(inverse);
- shadowInverse.simpleMatrix = inverseMatrix;
- }
- return true;
- }
- return false;
- }
-
- boolean hasPerspective() {
- return (simpleMatrix.mValues[6] != 0 || simpleMatrix.mValues[7] != 0 || simpleMatrix.mValues[8] != 1);
- }
-
- protected AffineTransform getAffineTransform() {
- // the AffineTransform constructor takes the value in a different order
- // for a matrix [ 0 1 2 ]
- // [ 3 4 5 ]
- // the order is 0, 3, 1, 4, 2, 5...
- return new AffineTransform(
- simpleMatrix.mValues[0],
- simpleMatrix.mValues[3],
- simpleMatrix.mValues[1],
- simpleMatrix.mValues[4],
- simpleMatrix.mValues[2],
- simpleMatrix.mValues[5]);
- }
-
- public PointF mapPoint(float x, float y) {
- return simpleMatrix.transform(new PointF(x, y));
- }
-
- public PointF mapPoint(PointF point) {
- return simpleMatrix.transform(point);
- }
-
- @Implementation
- protected boolean mapRect(RectF destination, RectF source) {
- final PointF leftTop = mapPoint(source.left, source.top);
- final PointF rightBottom = mapPoint(source.right, source.bottom);
- destination.set(
- Math.min(leftTop.x, rightBottom.x),
- Math.min(leftTop.y, rightBottom.y),
- Math.max(leftTop.x, rightBottom.x),
- Math.max(leftTop.y, rightBottom.y));
- return true;
- }
-
- @Implementation
- protected void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex, int pointCount) {
- for (int i = 0; i < pointCount; i++) {
- final PointF mapped = mapPoint(src[srcIndex + i * 2], src[srcIndex + i * 2 + 1]);
- dst[dstIndex + i * 2] = mapped.x;
- dst[dstIndex + i * 2 + 1] = mapped.y;
- }
- }
-
- @Implementation
- protected void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex, int vectorCount) {
- final float transX = simpleMatrix.mValues[Matrix.MTRANS_X];
- final float transY = simpleMatrix.mValues[Matrix.MTRANS_Y];
-
- simpleMatrix.mValues[Matrix.MTRANS_X] = 0;
- simpleMatrix.mValues[Matrix.MTRANS_Y] = 0;
-
- for (int i = 0; i < vectorCount; i++) {
- final PointF mapped = mapPoint(src[srcIndex + i * 2], src[srcIndex + i * 2 + 1]);
- dst[dstIndex + i * 2] = mapped.x;
- dst[dstIndex + i * 2 + 1] = mapped.y;
- }
-
- simpleMatrix.mValues[Matrix.MTRANS_X] = transX;
- simpleMatrix.mValues[Matrix.MTRANS_Y] = transY;
- }
-
- @Implementation
- protected float mapRadius(float radius) {
- float[] src = new float[] {radius, 0.f, 0.f, radius};
- mapVectors(src, 0, src, 0, 2);
-
- float l1 = (float) Math.hypot(src[0], src[1]);
- float l2 = (float) Math.hypot(src[2], src[3]);
- return (float) Math.sqrt(l1 * l2);
- }
-
- @Implementation
- protected boolean setRectToRect(RectF src, RectF dst, Matrix.ScaleToFit stf) {
- if (src.isEmpty()) {
- reset();
- return false;
- }
- return simpleMatrix.setRectToRect(src, dst, stf);
- }
-
- @Implementation
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof Matrix) {
- return getSimpleMatrix(((Matrix) obj)).equals(simpleMatrix);
- } else {
- return obj instanceof ShadowMatrix && obj.equals(simpleMatrix);
- }
- }
-
- @Implementation(minSdk = KITKAT)
- @Override
- public int hashCode() {
- return Objects.hashCode(simpleMatrix);
- }
-
- public String getDescription() {
- return "Matrix[pre=" + preOps + ", set=" + setOps + ", post=" + postOps + "]";
- }
-
- private static SimpleMatrix getSimpleMatrix(Matrix matrix) {
- final ShadowMatrix otherMatrix = Shadow.extract(matrix);
- return otherMatrix.simpleMatrix;
- }
-
- private boolean postConcat(SimpleMatrix matrix) {
- simpleMatrix = matrix.multiply(simpleMatrix);
- return true;
- }
-
- private boolean preConcat(SimpleMatrix matrix) {
- simpleMatrix = simpleMatrix.multiply(matrix);
- return true;
- }
-
- /**
- * A simple implementation of an immutable matrix.
- */
- private static class SimpleMatrix {
- private static final SimpleMatrix IDENTITY = newIdentityMatrix();
-
- private static SimpleMatrix newIdentityMatrix() {
- return new SimpleMatrix(
- new float[] {
- 1.0f, 0.0f, 0.0f,
- 0.0f, 1.0f, 0.0f,
- 0.0f, 0.0f, 1.0f,
- });
- }
-
- private final float[] mValues;
-
- SimpleMatrix(SimpleMatrix matrix) {
- mValues = Arrays.copyOf(matrix.mValues, matrix.mValues.length);
- }
-
- private SimpleMatrix(float[] values) {
- if (values.length != 9) {
- throw new ArrayIndexOutOfBoundsException();
- }
- mValues = Arrays.copyOf(values, 9);
- }
-
- public boolean isAffine() {
- return mValues[6] == 0.0f && mValues[7] == 0.0f && mValues[8] == 1.0f;
- }
-
- public boolean rectStaysRect() {
- final float m00 = mValues[0];
- final float m01 = mValues[1];
- final float m10 = mValues[3];
- final float m11 = mValues[4];
- return (m00 == 0 && m11 == 0 && m01 != 0 && m10 != 0) || (m00 != 0 && m11 != 0 && m01 == 0 && m10 == 0);
- }
-
- public void getValues(float[] values) {
- if (values.length < 9) {
- throw new ArrayIndexOutOfBoundsException();
- }
- System.arraycopy(mValues, 0, values, 0, 9);
- }
-
- public static SimpleMatrix translate(float dx, float dy) {
- return new SimpleMatrix(new float[] {
- 1.0f, 0.0f, dx,
- 0.0f, 1.0f, dy,
- 0.0f, 0.0f, 1.0f,
- });
- }
-
- public static SimpleMatrix scale(float sx, float sy, float px, float py) {
- return new SimpleMatrix(new float[] {
- sx, 0.0f, px * (1 - sx),
- 0.0f, sy, py * (1 - sy),
- 0.0f, 0.0f, 1.0f,
- });
- }
-
- public static SimpleMatrix scale(float sx, float sy) {
- return new SimpleMatrix(new float[] {
- sx, 0.0f, 0.0f,
- 0.0f, sy, 0.0f,
- 0.0f, 0.0f, 1.0f,
- });
- }
-
- public static SimpleMatrix rotate(float degrees, float px, float py) {
- final double radians = Math.toRadians(degrees);
- final float sin = (float) Math.sin(radians);
- final float cos = (float) Math.cos(radians);
- return sinCos(sin, cos, px, py);
- }
-
- public static SimpleMatrix rotate(float degrees) {
- final double radians = Math.toRadians(degrees);
- final float sin = (float) Math.sin(radians);
- final float cos = (float) Math.cos(radians);
- return sinCos(sin, cos);
- }
-
- public static SimpleMatrix sinCos(float sin, float cos, float px, float py) {
- return new SimpleMatrix(new float[] {
- cos, -sin, sin * py + (1 - cos) * px,
- sin, cos, -sin * px + (1 - cos) * py,
- 0.0f, 0.0f, 1.0f,
- });
- }
-
- public static SimpleMatrix sinCos(float sin, float cos) {
- return new SimpleMatrix(new float[] {
- cos, -sin, 0.0f,
- sin, cos, 0.0f,
- 0.0f, 0.0f, 1.0f,
- });
- }
-
- public static SimpleMatrix skew(float kx, float ky, float px, float py) {
- return new SimpleMatrix(new float[] {
- 1.0f, kx, -kx * py,
- ky, 1.0f, -ky * px,
- 0.0f, 0.0f, 1.0f,
- });
- }
-
- public static SimpleMatrix skew(float kx, float ky) {
- return new SimpleMatrix(new float[] {
- 1.0f, kx, 0.0f,
- ky, 1.0f, 0.0f,
- 0.0f, 0.0f, 1.0f,
- });
- }
-
- public SimpleMatrix multiply(SimpleMatrix matrix) {
- final float[] values = new float[9];
- for (int i = 0; i < values.length; ++i) {
- final int row = i / 3;
- final int col = i % 3;
- for (int j = 0; j < 3; ++j) {
- values[i] += mValues[row * 3 + j] * matrix.mValues[j * 3 + col];
- }
- }
- return new SimpleMatrix(values);
- }
-
- public SimpleMatrix invert() {
- final float invDet = inverseDeterminant();
- if (invDet == 0) {
- return null;
- }
-
- final float[] src = mValues;
- final float[] dst = new float[9];
- dst[0] = cross_scale(src[4], src[8], src[5], src[7], invDet);
- dst[1] = cross_scale(src[2], src[7], src[1], src[8], invDet);
- dst[2] = cross_scale(src[1], src[5], src[2], src[4], invDet);
-
- dst[3] = cross_scale(src[5], src[6], src[3], src[8], invDet);
- dst[4] = cross_scale(src[0], src[8], src[2], src[6], invDet);
- dst[5] = cross_scale(src[2], src[3], src[0], src[5], invDet);
-
- dst[6] = cross_scale(src[3], src[7], src[4], src[6], invDet);
- dst[7] = cross_scale(src[1], src[6], src[0], src[7], invDet);
- dst[8] = cross_scale(src[0], src[4], src[1], src[3], invDet);
- return new SimpleMatrix(dst);
- }
-
- public PointF transform(PointF point) {
- return new PointF(
- point.x * mValues[0] + point.y * mValues[1] + mValues[2],
- point.x * mValues[3] + point.y * mValues[4] + mValues[5]);
- }
-
- // See: https://android.googlesource.com/platform/frameworks/base/+/6fca81de9b2079ec88e785f58bf49bf1f0c105e2/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java
- protected boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) {
- if (dst.isEmpty()) {
- mValues[0] =
- mValues[1] =
- mValues[2] = mValues[3] = mValues[4] = mValues[5] = mValues[6] = mValues[7] = 0;
- mValues[8] = 1;
- } else {
- float tx = dst.width() / src.width();
- float sx = dst.width() / src.width();
- float ty = dst.height() / src.height();
- float sy = dst.height() / src.height();
- boolean xLarger = false;
-
- if (stf != ScaleToFit.FILL) {
- if (sx > sy) {
- xLarger = true;
- sx = sy;
- } else {
- sy = sx;
- }
- }
-
- tx = dst.left - src.left * sx;
- ty = dst.top - src.top * sy;
- if (stf == ScaleToFit.CENTER || stf == ScaleToFit.END) {
- float diff;
-
- if (xLarger) {
- diff = dst.width() - src.width() * sy;
- } else {
- diff = dst.height() - src.height() * sy;
- }
-
- if (stf == ScaleToFit.CENTER) {
- diff = diff / 2;
- }
-
- if (xLarger) {
- tx += diff;
- } else {
- ty += diff;
- }
- }
-
- mValues[0] = sx;
- mValues[4] = sy;
- mValues[2] = tx;
- mValues[5] = ty;
- mValues[1] = mValues[3] = mValues[6] = mValues[7] = 0;
- }
- // shared cleanup
- mValues[8] = 1;
- return true;
- }
-
- @Override
- public boolean equals(Object o) {
- return this == o || (o instanceof SimpleMatrix && equals((SimpleMatrix) o));
- }
-
- @SuppressWarnings("NonOverridingEquals")
- public boolean equals(SimpleMatrix matrix) {
- if (matrix == null) {
- return false;
- }
- for (int i = 0; i < mValues.length; i++) {
- if (!isNearlyZero(matrix.mValues[i] - mValues[i])) {
- return false;
- }
- }
- return true;
- }
+ public abstract String getDescription();
+ /** A {@link ShadowPicker} that always selects the legacy ShadowPath */
+ public static class Picker implements ShadowPicker<ShadowMatrix> {
@Override
- public int hashCode() {
- return Arrays.hashCode(mValues);
- }
-
- private static boolean isNearlyZero(float value) {
- return Math.abs(value) < EPSILON;
- }
-
- private static float cross(float a, float b, float c, float d) {
- return a * b - c * d;
- }
-
- private static float cross_scale(float a, float b, float c, float d, float scale) {
- return cross(a, b, c, d) * scale;
- }
-
- private float inverseDeterminant() {
- final float determinant = mValues[0] * cross(mValues[4], mValues[8], mValues[5], mValues[7]) +
- mValues[1] * cross(mValues[5], mValues[6], mValues[3], mValues[8]) +
- mValues[2] * cross(mValues[3], mValues[7], mValues[4], mValues[6]);
- return isNearlyZero(determinant) ? 0.0f : 1.0f / determinant;
+ public Class<? extends ShadowMatrix> pickShadowClass() {
+ return ShadowLegacyMatrix.class;
}
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java
index 809b5cc5b..252bc427d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaController.java
@@ -4,6 +4,7 @@ import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.PendingIntent;
import android.media.MediaMetadata;
import android.media.Rating;
@@ -12,6 +13,7 @@ import android.media.session.MediaController.Callback;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.PlaybackState;
import android.os.Bundle;
+import android.os.Handler;
import java.util.ArrayList;
import java.util.List;
import org.robolectric.annotation.Implementation;
@@ -30,6 +32,7 @@ public class ShadowMediaController {
private PlaybackInfo playbackInfo;
private MediaMetadata mediaMetadata;
private PendingIntent sessionActivity;
+ private Bundle extras;
/**
* A value of RATING_NONE for ratingType indicates that rating media is not supported by the media
@@ -122,14 +125,26 @@ public class ShadowMediaController {
return sessionActivity;
}
+ /** Saves the extras to control the return value of {@link MediaController#getExtras()}. */
+ public void setExtras(Bundle extras) {
+ this.extras = extras;
+ }
+
+ /** Gets the extras set via {@link #extras}. */
+ @Implementation
+ protected Bundle getExtras() {
+ return extras;
+ }
+
/**
* Register callback and store it in the shadow to make it easier to check the state of the
- * registered callbacks.
+ * registered callbacks. Handler is just passed on to the real class.
*/
@Implementation
- protected void registerCallback(@NonNull Callback callback) {
+ protected void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
callbacks.add(callback);
- reflector(MediaControllerReflector.class, realMediaController).registerCallback(callback);
+ reflector(MediaControllerReflector.class, realMediaController)
+ .registerCallback(callback, handler);
}
/**
@@ -192,7 +207,7 @@ public class ShadowMediaController {
interface MediaControllerReflector {
@Direct
- void registerCallback(@NonNull Callback callback);
+ void registerCallback(@NonNull Callback callback, @Nullable Handler handler);
@Direct
void unregisterCallback(@NonNull Callback callback);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java
index be962b324..31fc79634 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaStore.java
@@ -35,7 +35,11 @@ public class ShadowMediaStore {
@Implementation
protected static Bitmap getBitmap(ContentResolver cr, Uri url) {
- return ShadowBitmapFactory.create(url.toString(), null, null);
+ if (ShadowView.useRealGraphics()) {
+ return Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+ } else {
+ return ShadowBitmapFactory.create(url.toString(), null, null);
+ }
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java
index 39e3d9b02..4f03aa337 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNetworkCapabilities.java
@@ -19,7 +19,7 @@ import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
/** Robolectic provides overrides for fetching and updating transport. */
-@Implements(value = NetworkCapabilities.class, minSdk = LOLLIPOP)
+@Implements(value = NetworkCapabilities.class, minSdk = LOLLIPOP, looseSignatures = true)
public class ShadowNetworkCapabilities {
@RealObject protected NetworkCapabilities realNetworkCapabilities;
@@ -90,10 +90,12 @@ public class ShadowNetworkCapabilities {
/** Sets the LinkDownstreamBandwidthKbps of the NetworkCapabilities. */
@HiddenApi
- @Implementation(minSdk = Q)
- public NetworkCapabilities setLinkDownstreamBandwidthKbps(int kbps) {
+ @Implementation
+ public Object setLinkDownstreamBandwidthKbps(Object kbps) {
+ // Loose signatures is necessary because the return type of setLinkDownstreamBandwidthKbps
+ // changed from void to NetworkCapabilities starting from API 28 (Pie)
return reflector(NetworkCapabilitiesReflector.class, realNetworkCapabilities)
- .setLinkDownstreamBandwidthKbps(kbps);
+ .setLinkDownstreamBandwidthKbps((int) kbps);
}
@ForType(NetworkCapabilities.class)
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java
index dfe78a2e4..7310b5cac 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeAllocationRegistry.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNoopNativeAllocationRegistry.java
@@ -6,8 +6,13 @@ import libcore.util.NativeAllocationRegistry;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-@Implements(value = NativeAllocationRegistry.class, minSdk = N, isInAndroidSdk = false, looseSignatures = true)
-public class ShadowNativeAllocationRegistry {
+/** Shadow for {@link NativeAllocationRegistry} that is a no-op. */
+@Implements(
+ value = NativeAllocationRegistry.class,
+ minSdk = N,
+ isInAndroidSdk = false,
+ looseSignatures = true)
+public class ShadowNoopNativeAllocationRegistry {
@Implementation
protected Runnable registerNativeAllocation(Object referent, Object allocator) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java
index 8cf21f9b8..3b9889fc7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNumberPicker.java
@@ -12,61 +12,25 @@ import org.robolectric.util.reflector.ForType;
@Implements(value = NumberPicker.class)
public class ShadowNumberPicker extends ShadowLinearLayout {
@RealObject private NumberPicker realNumberPicker;
- private int value;
- private int minValue;
- private int maxValue;
- private boolean wrapSelectorWheel;
private String[] displayedValues;
private NumberPicker.OnValueChangeListener onValueChangeListener;
@Implementation
- protected void setValue(int value) {
- this.value = value;
- }
-
- @Implementation
- protected int getValue() {
- return value;
- }
-
- @Implementation
protected void setDisplayedValues(String[] displayedValues) {
- this.displayedValues = displayedValues;
+ if (ShadowView.useRealGraphics()) {
+ reflector(NumberPickerReflector.class, realNumberPicker).setDisplayedValues(displayedValues);
+ } else {
+ this.displayedValues = displayedValues;
+ }
}
@Implementation
protected String[] getDisplayedValues() {
- return displayedValues;
- }
-
- @Implementation
- protected void setMinValue(int minValue) {
- this.minValue = minValue;
- }
-
- @Implementation
- protected void setMaxValue(int maxValue) {
- this.maxValue = maxValue;
- }
-
- @Implementation
- protected int getMinValue() {
- return this.minValue;
- }
-
- @Implementation
- protected int getMaxValue() {
- return this.maxValue;
- }
-
- @Implementation
- protected void setWrapSelectorWheel(boolean wrapSelectorWheel) {
- this.wrapSelectorWheel = wrapSelectorWheel;
- }
-
- @Implementation
- protected boolean getWrapSelectorWheel() {
- return wrapSelectorWheel;
+ if (ShadowView.useRealGraphics()) {
+ return reflector(NumberPickerReflector.class, realNumberPicker).getDisplayedValues();
+ } else {
+ return displayedValues;
+ }
}
@Implementation
@@ -84,5 +48,11 @@ public class ShadowNumberPicker extends ShadowLinearLayout {
@Direct
void setOnValueChangedListener(NumberPicker.OnValueChangeListener listener);
+
+ @Direct
+ void setDisplayedValues(String[] displayedValues);
+
+ @Direct
+ String[] getDisplayedValues();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOutline.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOutline.java
deleted file mode 100644
index d09466817..000000000
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOutline.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.robolectric.shadows;
-
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-
-import android.graphics.Outline;
-import android.graphics.Path;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-@Implements(value = Outline.class, minSdk = LOLLIPOP)
-public class ShadowOutline {
-
- @Implementation
- protected void setConvexPath(Path convexPath) {}
-} \ No newline at end of file
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java
index 889736f05..459130f28 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPath.java
@@ -1,163 +1,29 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.JELLY_BEAN;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static org.robolectric.shadow.api.Shadow.extract;
-import static org.robolectric.shadows.ShadowPath.Point.Type.LINE_TO;
-import static org.robolectric.shadows.ShadowPath.Point.Type.MOVE_TO;
-import android.graphics.Matrix;
import android.graphics.Path;
-import android.graphics.Path.Direction;
import android.graphics.RectF;
-import android.util.Log;
-import java.awt.geom.AffineTransform;
-import java.awt.geom.Arc2D;
-import java.awt.geom.Area;
-import java.awt.geom.Ellipse2D;
-import java.awt.geom.GeneralPath;
-import java.awt.geom.Path2D;
-import java.awt.geom.PathIterator;
-import java.awt.geom.Point2D;
-import java.awt.geom.Rectangle2D;
-import java.awt.geom.RoundRectangle2D;
-import java.util.ArrayList;
import java.util.List;
-import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
+import org.robolectric.shadow.api.ShadowPicker;
+import org.robolectric.shadows.ShadowPath.Picker;
-/**
- * The shadow only supports straight-line paths.
- */
+/** Base class for {@link ShadowPath} classes. */
@SuppressWarnings({"UnusedDeclaration"})
-@Implements(Path.class)
-public class ShadowPath {
- private static final String TAG = ShadowPath.class.getSimpleName();
- private static final float EPSILON = 1e-4f;
-
- @RealObject private Path realObject;
-
- private List<Point> points = new ArrayList<>();
-
- private float mLastX = 0;
- private float mLastY = 0;
- private Path2D mPath = new Path2D.Double();
- private boolean mCachedIsEmpty = true;
- private Path.FillType mFillType = Path.FillType.WINDING;
- protected boolean isSimplePath;
-
- @Implementation
- protected void __constructor__(Path path) {
- ShadowPath shadowPath = extract(path);
- points = new ArrayList<>(shadowPath.getPoints());
- mPath.append(shadowPath.mPath, /*connect=*/ false);
- mFillType = shadowPath.getFillType();
- }
-
- Path2D getJavaShape() {
- return mPath;
- }
-
- @Implementation
- protected void moveTo(float x, float y) {
- mPath.moveTo(mLastX = x, mLastY = y);
-
- // Legacy recording behavior
- Point p = new Point(x, y, MOVE_TO);
- points.add(p);
- }
-
- @Implementation
- protected void lineTo(float x, float y) {
- if (!hasPoints()) {
- mPath.moveTo(mLastX = 0, mLastY = 0);
- }
- mPath.lineTo(mLastX = x, mLastY = y);
-
- // Legacy recording behavior
- Point point = new Point(x, y, LINE_TO);
- points.add(point);
- }
-
- @Implementation
- protected void quadTo(float x1, float y1, float x2, float y2) {
- isSimplePath = false;
- if (!hasPoints()) {
- moveTo(0, 0);
- }
- mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
- }
-
- @Implementation
- protected void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
- if (!hasPoints()) {
- mPath.moveTo(0, 0);
- }
- mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
- }
-
- private boolean hasPoints() {
- return !mPath.getPathIterator(null).isDone();
- }
-
- @Implementation
- protected void reset() {
- mPath.reset();
- mLastX = 0;
- mLastY = 0;
-
- // Legacy recording behavior
- points.clear();
- }
-
- @Implementation(minSdk = LOLLIPOP)
- protected float[] approximate(float acceptableError) {
- PathIterator iterator = mPath.getPathIterator(null, acceptableError);
-
- float segment[] = new float[6];
- float totalLength = 0;
- ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>();
- Point2D.Float previousPoint = null;
- while (!iterator.isDone()) {
- int type = iterator.currentSegment(segment);
- Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]);
- // MoveTo shouldn't affect the length
- if (previousPoint != null && type != PathIterator.SEG_MOVETO) {
- totalLength += (float) currentPoint.distance(previousPoint);
- }
- previousPoint = currentPoint;
- points.add(currentPoint);
- iterator.next();
- }
-
- int nPoints = points.size();
- float[] result = new float[nPoints * 3];
- previousPoint = null;
- // Distance that we've covered so far. Used to calculate the fraction of the path that
- // we've covered up to this point.
- float walkedDistance = .0f;
- for (int i = 0; i < nPoints; i++) {
- Point2D.Float point = points.get(i);
- float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
- walkedDistance += distance;
- result[i * 3] = walkedDistance / totalLength;
- result[i * 3 + 1] = point.x;
- result[i * 3 + 2] = point.y;
-
- previousPoint = point;
- }
-
- return result;
- }
+@Implements(value = Path.class, shadowPicker = Picker.class)
+public abstract class ShadowPath {
/**
* @return all the points that have been added to the {@code Path}
*/
- public List<Point> getPoints() {
- return points;
- }
+ public abstract List<Point> getPoints();
+
+ /**
+ * Fills the given {@link RectF} with the path bounds.
+ *
+ * @param bounds the RectF to be filled.
+ */
+ public abstract void fillBounds(RectF bounds);
public static class Point {
private final float x;
@@ -215,400 +81,11 @@ public class ShadowPath {
}
}
- @Implementation
- protected void rewind() {
- // call out to reset since there's nothing to optimize in
- // terms of data structs.
- reset();
- }
-
- @Implementation
- protected void set(Path src) {
- mPath.reset();
-
- ShadowPath shadowSrc = extract(src);
- setFillType(shadowSrc.mFillType);
- mPath.append(shadowSrc.mPath, false /*connect*/);
- }
-
- @Implementation(minSdk = KITKAT)
- protected boolean op(Path path1, Path path2, Path.Op op) {
- Log.w(TAG, "android.graphics.Path#op() not supported yet.");
- return false;
- }
-
- @Implementation(minSdk = LOLLIPOP)
- protected boolean isConvex() {
- Log.w(TAG, "android.graphics.Path#isConvex() not supported yet.");
- return true;
- }
-
- @Implementation
- protected Path.FillType getFillType() {
- return mFillType;
- }
-
- @Implementation
- protected void setFillType(Path.FillType fillType) {
- mFillType = fillType;
- mPath.setWindingRule(getWindingRule(fillType));
- }
-
- /**
- * Returns the Java2D winding rules matching a given Android {@link FillType}.
- *
- * @param type the android fill type
- * @return the matching java2d winding rule.
- */
- private static int getWindingRule(Path.FillType type) {
- switch (type) {
- case WINDING:
- case INVERSE_WINDING:
- return GeneralPath.WIND_NON_ZERO;
- case EVEN_ODD:
- case INVERSE_EVEN_ODD:
- return GeneralPath.WIND_EVEN_ODD;
-
- default:
- assert false;
- return GeneralPath.WIND_NON_ZERO;
- }
- }
-
- @Implementation
- protected boolean isInverseFillType() {
- throw new UnsupportedOperationException("isInverseFillType");
- }
-
- @Implementation
- protected void toggleInverseFillType() {
- throw new UnsupportedOperationException("toggleInverseFillType");
- }
-
- @Implementation
- protected boolean isEmpty() {
- if (!mCachedIsEmpty) {
- return false;
- }
-
- float[] coords = new float[6];
- mCachedIsEmpty = Boolean.TRUE;
- for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
- // int type = it.currentSegment(coords);
- // if (type != PathIterator.SEG_MOVETO) {
- // Once we know that the path is not empty, we do not need to check again unless
- // Path#reset is called.
- mCachedIsEmpty = false;
- return false;
- // }
- }
-
- return true;
- }
-
- @Implementation
- protected boolean isRect(RectF rect) {
- // create an Area that can test if the path is a rect
- Area area = new Area(mPath);
- if (area.isRectangular()) {
- if (rect != null) {
- fillBounds(rect);
- }
-
- return true;
- }
-
- return false;
- }
-
- @Implementation
- protected void computeBounds(RectF bounds, boolean exact) {
- fillBounds(bounds);
- }
-
- @Implementation
- protected void incReserve(int extraPtCount) {
- throw new UnsupportedOperationException("incReserve");
- }
-
- @Implementation
- protected void rMoveTo(float dx, float dy) {
- dx += mLastX;
- dy += mLastY;
- mPath.moveTo(mLastX = dx, mLastY = dy);
- }
-
- @Implementation
- protected void rLineTo(float dx, float dy) {
- if (!hasPoints()) {
- mPath.moveTo(mLastX = 0, mLastY = 0);
- }
-
- if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
- // The delta is so small that this shouldn't generate a line
- return;
- }
-
- dx += mLastX;
- dy += mLastY;
- mPath.lineTo(mLastX = dx, mLastY = dy);
- }
-
- @Implementation
- protected void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
- if (!hasPoints()) {
- mPath.moveTo(mLastX = 0, mLastY = 0);
- }
- dx1 += mLastX;
- dy1 += mLastY;
- dx2 += mLastX;
- dy2 += mLastY;
- mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
- }
-
- @Implementation
- protected void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
- if (!hasPoints()) {
- mPath.moveTo(mLastX = 0, mLastY = 0);
- }
- x1 += mLastX;
- y1 += mLastY;
- x2 += mLastX;
- y2 += mLastY;
- x3 += mLastX;
- y3 += mLastY;
- mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
- }
-
- @Implementation
- protected void arcTo(RectF oval, float startAngle, float sweepAngle) {
- arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, false);
- }
-
- @Implementation
- protected void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) {
- arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
- }
-
- @Implementation(minSdk = LOLLIPOP)
- protected void arcTo(
- float left,
- float top,
- float right,
- float bottom,
- float startAngle,
- float sweepAngle,
- boolean forceMoveTo) {
- isSimplePath = false;
- Arc2D arc =
- new Arc2D.Float(
- left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN);
- mPath.append(arc, true /*connect*/);
- if (hasPoints()) {
- resetLastPointFromPath();
- }
- }
-
- @Implementation
- protected void close() {
- if (!hasPoints()) {
- mPath.moveTo(mLastX = 0, mLastY = 0);
- }
- mPath.closePath();
- }
-
- @Implementation
- protected void addRect(RectF rect, Direction dir) {
- addRect(rect.left, rect.top, rect.right, rect.bottom, dir);
- }
-
- @Implementation
- protected void addRect(float left, float top, float right, float bottom, Path.Direction dir) {
- moveTo(left, top);
-
- switch (dir) {
- case CW:
- lineTo(right, top);
- lineTo(right, bottom);
- lineTo(left, bottom);
- break;
- case CCW:
- lineTo(left, bottom);
- lineTo(right, bottom);
- lineTo(right, top);
- break;
- }
-
- close();
-
- resetLastPointFromPath();
- }
-
- @Implementation(minSdk = LOLLIPOP)
- protected void addOval(float left, float top, float right, float bottom, Path.Direction dir) {
- mPath.append(new Ellipse2D.Float(left, top, right - left, bottom - top), false);
- }
-
- @Implementation
- protected void addCircle(float x, float y, float radius, Path.Direction dir) {
- mPath.append(new Ellipse2D.Float(x - radius, y - radius, radius * 2, radius * 2), false);
- }
-
- @Implementation(minSdk = LOLLIPOP)
- protected void addArc(
- float left, float top, float right, float bottom, float startAngle, float sweepAngle) {
- mPath.append(
- new Arc2D.Float(
- left, top, right - left, bottom - top, -startAngle, -sweepAngle, Arc2D.OPEN),
- false);
- }
-
- @Implementation(minSdk = JELLY_BEAN)
- protected void addRoundRect(RectF rect, float rx, float ry, Direction dir) {
- addRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, dir);
- }
-
- @Implementation(minSdk = JELLY_BEAN)
- protected void addRoundRect(RectF rect, float[] radii, Direction dir) {
- if (rect == null) {
- throw new NullPointerException("need rect parameter");
- }
- addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir);
- }
-
- @Implementation(minSdk = LOLLIPOP)
- protected void addRoundRect(
- float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) {
- mPath.append(
- new RoundRectangle2D.Float(left, top, right - left, bottom - top, rx * 2, ry * 2), false);
- }
-
- @Implementation(minSdk = LOLLIPOP)
- protected void addRoundRect(
- float left, float top, float right, float bottom, float[] radii, Path.Direction dir) {
- if (radii.length < 8) {
- throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values");
- }
- isSimplePath = false;
-
- float[] cornerDimensions = new float[radii.length];
- for (int i = 0; i < radii.length; i++) {
- cornerDimensions[i] = 2 * radii[i];
- }
- mPath.append(
- new RoundRectangle(left, top, right - left, bottom - top, cornerDimensions), false);
- }
-
- @Implementation
- protected void addPath(Path src, float dx, float dy) {
- isSimplePath = false;
- ShadowPath.addPath(realObject, src, AffineTransform.getTranslateInstance(dx, dy));
- }
-
- @Implementation
- protected void addPath(Path src) {
- isSimplePath = false;
- ShadowPath.addPath(realObject, src, null);
- }
-
- @Implementation
- protected void addPath(Path src, Matrix matrix) {
- if (matrix == null) {
- return;
- }
- ShadowPath shadowSrc = extract(src);
- if (!shadowSrc.isSimplePath) isSimplePath = false;
-
- ShadowMatrix shadowMatrix = extract(matrix);
- ShadowPath.addPath(realObject, src, shadowMatrix.getAffineTransform());
- }
-
- private static void addPath(Path destPath, Path srcPath, AffineTransform transform) {
- if (destPath == null) {
- return;
- }
-
- if (srcPath == null) {
- return;
- }
-
- ShadowPath shadowDestPath = extract(destPath);
- ShadowPath shadowSrcPath = extract(srcPath);
- if (transform != null) {
- shadowDestPath.mPath.append(shadowSrcPath.mPath.getPathIterator(transform), false);
- } else {
- shadowDestPath.mPath.append(shadowSrcPath.mPath, false);
- }
- }
-
- @Implementation
- protected void offset(float dx, float dy, Path dst) {
- if (dst != null) {
- dst.set(realObject);
- } else {
- dst = realObject;
- }
- dst.offset(dx, dy);
- }
-
- @Implementation
- protected void offset(float dx, float dy) {
- GeneralPath newPath = new GeneralPath();
-
- PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
-
- newPath.append(iterator, false /*connect*/);
- mPath = newPath;
- }
-
- @Implementation
- protected void setLastPoint(float dx, float dy) {
- mLastX = dx;
- mLastY = dy;
- }
-
- @Implementation
- protected void transform(Matrix matrix, Path dst) {
- ShadowMatrix shadowMatrix = extract(matrix);
-
- if (shadowMatrix.hasPerspective()) {
- Log.w(TAG, "android.graphics.Path#transform() only supports affine transformations.");
- }
-
- GeneralPath newPath = new GeneralPath();
-
- PathIterator iterator = mPath.getPathIterator(shadowMatrix.getAffineTransform());
- newPath.append(iterator, false /*connect*/);
-
- if (dst != null) {
- ShadowPath shadowPath = extract(dst);
- shadowPath.mPath = newPath;
- } else {
- mPath = newPath;
+ /** A {@link ShadowPicker} that always selects the legacy ShadowPath */
+ public static class Picker implements ShadowPicker<ShadowPath> {
+ @Override
+ public Class<? extends ShadowPath> pickShadowClass() {
+ return ShadowLegacyPath.class;
}
}
-
- @Implementation
- protected void transform(Matrix matrix) {
- transform(matrix, null);
- }
-
- /**
- * Fills the given {@link RectF} with the path bounds.
- *
- * @param bounds the RectF to be filled.
- */
- public void fillBounds(RectF bounds) {
- Rectangle2D rect = mPath.getBounds2D();
- bounds.left = (float) rect.getMinX();
- bounds.right = (float) rect.getMaxX();
- bounds.top = (float) rect.getMinY();
- bounds.bottom = (float) rect.getMaxY();
- }
-
- private void resetLastPointFromPath() {
- Point2D last = mPath.getCurrentPoint();
- mLastX = (float) last.getX();
- mLastY = (float) last.getY();
- }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java
index 5bc2b9732..408778024 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPathMeasure.java
@@ -15,7 +15,7 @@ public class ShadowPathMeasure {
@Implementation
protected void __constructor__(Path path, boolean forceClosed) {
if (path != null) {
- ShadowPath shadowPath = (ShadowPath) Shadow.extract(path);
+ ShadowLegacyPath shadowPath = Shadow.extract(path);
mOriginalPathIterator =
new CachedPathIteratorFactory(shadowPath.getJavaShape().getPathIterator(null));
}
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 df4e15b58..50f8adf62 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
@@ -430,14 +430,10 @@ public final class ShadowPausedLooper extends ShadowLooper {
setLooperExecutor(this);
isPaused = true;
runLatch.countDown();
- while (true) {
+ while (isPaused) {
try {
Runnable runnable = executionQueue.take();
runnable.run();
- if (runnable instanceof UnPauseRunnable) {
- setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
- return;
- }
} catch (InterruptedException e) {
// ignore
}
@@ -448,6 +444,7 @@ public final class ShadowPausedLooper extends ShadowLooper {
private class UnPauseRunnable extends ControlRunnable {
@Override
public void run() {
+ setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
isPaused = false;
runLatch.countDown();
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java
index 232132945..9411ff1c8 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPhoneWindow.java
@@ -1,11 +1,13 @@
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.R;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.graphics.drawable.Drawable;
import android.view.Gravity;
import android.view.Window;
+import androidx.annotation.RequiresApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
@@ -17,8 +19,8 @@ import org.robolectric.util.reflector.ForType;
@Implements(className = "com.android.internal.policy.PhoneWindow", isInAndroidSdk = false,
minSdk = M, looseSignatures = true)
public class ShadowPhoneWindow extends ShadowWindow {
- @SuppressWarnings("UnusedDeclaration")
protected @RealObject Window realWindow;
+ protected boolean decorFitsSystemWindows = true;
@Implementation(minSdk = M)
public void setTitle(CharSequence title) {
@@ -37,11 +39,29 @@ public class ShadowPhoneWindow extends ShadowWindow {
return Gravity.CENTER | Gravity.BOTTOM;
}
+ @Implementation(minSdk = R)
+ protected void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
+ this.decorFitsSystemWindows = decorFitsSystemWindows;
+ reflector(DirectPhoneWindowReflector.class, realWindow)
+ .setDecorFitsSystemWindows(decorFitsSystemWindows);
+ }
+
+ /**
+ * Returns true with the last value passed to {@link #setDecorFitsSystemWindows(boolean)}, or the
+ * default value (true).
+ */
+ @RequiresApi(R)
+ public boolean getDecorFitsSystemWindows() {
+ return decorFitsSystemWindows;
+ }
+
@ForType(className = "com.android.internal.policy.PhoneWindow", direct = true)
interface DirectPhoneWindowReflector {
void setTitle(CharSequence title);
void setBackgroundDrawable(Drawable drawable);
+
+ void setDecorFitsSystemWindows(boolean decorFitsSystemWindows);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java
index 6cf1dbf86..c85cf8f9c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPosix.java
@@ -11,10 +11,14 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
-@Implements(className = "libcore.io.Posix", maxSdk = Build.VERSION_CODES.N_MR1, isInAndroidSdk = false)
+/** Shadow for {@link libcore.io.Posix} */
+@Implements(
+ className = "libcore.io.Posix",
+ maxSdk = Build.VERSION_CODES.N_MR1,
+ isInAndroidSdk = false)
public class ShadowPosix {
@Implementation
- public static void mkdir(String path, int mode) throws ErrnoException {
+ public void mkdir(String path, int mode) throws ErrnoException {
new File(path).mkdirs();
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java
index 93f90b2a2..f3e735522 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPreference.java
@@ -6,7 +6,6 @@ import android.preference.Preference;
import android.preference.PreferenceManager;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
-import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
@Implements(Preference.class)
@@ -23,8 +22,6 @@ public class ShadowPreference {
@ForType(Preference.class)
interface PreferenceReflector {
-
- @Direct
void onAttachedToHierarchy(PreferenceManager preferenceManager);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java
index 1048d56d5..2f91d2d52 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowRecordingCanvas.java
@@ -7,7 +7,7 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@Implements(value = RecordingCanvas.class, isInAndroidSdk = false, minSdk = Q)
-public class ShadowRecordingCanvas extends ShadowCanvas {
+public class ShadowRecordingCanvas extends ShadowLegacyCanvas {
@Implementation
protected static long nCreateDisplayListCanvas(long node, int width, int height) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java
index c7f920551..ca5032f55 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowScaleGestureDetector.java
@@ -5,27 +5,32 @@ import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.ReflectorObject;
+import org.robolectric.util.reflector.Direct;
+import org.robolectric.util.reflector.ForType;
@SuppressWarnings({"UnusedDeclaration"})
@Implements(ScaleGestureDetector.class)
public class ShadowScaleGestureDetector {
+ @ReflectorObject ScaleGestureDetectorReflector scaleGestureDetectorReflector;
private MotionEvent onTouchEventMotionEvent;
private ScaleGestureDetector.OnScaleGestureListener listener;
- private float scaleFactor = 1;
- private float focusX;
- private float focusY;
+ private Float scaleFactor;
+ private Float focusX;
+ private Float focusY;
@Implementation
protected void __constructor__(
Context context, ScaleGestureDetector.OnScaleGestureListener listener) {
+ scaleGestureDetectorReflector.__constructor__(context, listener);
this.listener = listener;
}
@Implementation
protected boolean onTouchEvent(MotionEvent event) {
onTouchEventMotionEvent = event;
- return true;
+ return scaleGestureDetectorReflector.onTouchEvent(event);
}
public MotionEvent getOnTouchEventMotionEvent() {
@@ -34,9 +39,9 @@ public class ShadowScaleGestureDetector {
public void reset() {
onTouchEventMotionEvent = null;
- scaleFactor = 1;
- focusX = 0;
- focusY = 0;
+ scaleFactor = null;
+ focusX = null;
+ focusY = null;
}
public ScaleGestureDetector.OnScaleGestureListener getListener() {
@@ -49,7 +54,7 @@ public class ShadowScaleGestureDetector {
@Implementation
protected float getScaleFactor() {
- return scaleFactor;
+ return scaleFactor != null ? scaleFactor : scaleGestureDetectorReflector.getScaleFactor();
}
public void setFocusXY(float focusX, float focusY) {
@@ -59,11 +64,29 @@ public class ShadowScaleGestureDetector {
@Implementation
protected float getFocusX() {
- return focusX;
+ return focusX != null ? focusX : scaleGestureDetectorReflector.getFocusX();
}
@Implementation
protected float getFocusY() {
- return focusY;
+ return focusY != null ? focusY : scaleGestureDetectorReflector.getFocusY();
+ }
+
+ @ForType(ScaleGestureDetector.class)
+ private interface ScaleGestureDetectorReflector {
+ @Direct
+ void __constructor__(Context context, ScaleGestureDetector.OnScaleGestureListener listener);
+
+ @Direct
+ boolean onTouchEvent(MotionEvent event);
+
+ @Direct
+ float getScaleFactor();
+
+ @Direct
+ float getFocusX();
+
+ @Direct
+ float getFocusY();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java
index 204dc4cb9..5c985a5c2 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSearchManager.java
@@ -1,17 +1,49 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.M;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.annotation.SystemApi;
import android.app.SearchManager;
import android.app.SearchableInfo;
import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
@Implements(SearchManager.class)
public class ShadowSearchManager {
+ @RealObject private SearchManager searchManager;
+
@Implementation
protected SearchableInfo getSearchableInfo(ComponentName componentName) {
// Prevent Robolectric from calling through
return null;
}
+
+ @Implementation(minSdk = M)
+ @SystemApi
+ protected void launchAssist(Bundle bundle) {
+ Intent intent = new Intent(Intent.ACTION_ASSIST);
+ intent.putExtras(bundle);
+ getContext().sendBroadcast(intent);
+ }
+
+ private Context getContext() {
+ return reflector(ReflectorSearchManager.class, searchManager).getContext();
+ }
+
+ /** Reflector interface for {@link SearchManager}'s internals. */
+ @ForType(SearchManager.class)
+ private interface ReflectorSearchManager {
+
+ @Accessor("mContext")
+ Context getContext();
+ }
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java
index 5aa44afd2..c973fe3c7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSensorManager.java
@@ -9,8 +9,10 @@ import android.hardware.Sensor;
import android.hardware.SensorDirectChannel;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
+import android.hardware.SensorEventListener2;
import android.hardware.SensorManager;
import android.os.Handler;
+import android.os.Looper;
import android.os.MemoryFile;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
@@ -146,6 +148,27 @@ public class ShadowSensorManager {
}
}
+ @Implementation(minSdk = KITKAT)
+ protected boolean flush(SensorEventListener listener) {
+ // ShadowSensorManager doesn't queue up any sensor events, so nothing actually needs to be
+ // flushed. Just call onFlushCompleted for each sensor that would have been flushed.
+ new Handler(Looper.getMainLooper())
+ .post(
+ () -> {
+ // Go through each sensor that the listener is registered for, and call
+ // onFlushCompleted on each listener registered for that sensor.
+ for (Sensor sensor : listeners.get(listener)) {
+ for (SensorEventListener registeredListener : getListeners()) {
+ if ((registeredListener instanceof SensorEventListener2)
+ && listeners.containsEntry(registeredListener, sensor)) {
+ ((SensorEventListener2) registeredListener).onFlushCompleted(sensor);
+ }
+ }
+ }
+ });
+ return listeners.containsKey(listener);
+ }
+
public SensorEvent createSensorEvent() {
return ReflectionHelpers.callConstructor(SensorEvent.class);
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java
index 43a758f48..552d3101f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSettings.java
@@ -9,6 +9,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.provider.Settings.Secure.LOCATION_MODE_OFF;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.content.ContentResolver;
import android.content.Context;
@@ -32,8 +33,8 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
-import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
@SuppressWarnings({"UnusedDeclaration"})
@Implements(Settings.class)
@@ -267,11 +268,7 @@ public class ShadowSettings {
&& RuntimeEnvironment.getApiLevel() >= KITKAT
&& RuntimeEnvironment.getApiLevel() < P) {
// Map from to underlying location provider storage API to location mode
- return Shadow.directlyOn(
- Settings.Secure.class,
- "getLocationModeForUser",
- ClassParameter.from(ContentResolver.class, cr),
- ClassParameter.from(int.class, 0));
+ return reflector(SettingsSecureReflector.class).getLocationModeForUser(cr, 0);
}
return get(Integer.class, name).orElseThrow(() -> new SettingNotFoundException(name));
@@ -283,11 +280,7 @@ public class ShadowSettings {
&& RuntimeEnvironment.getApiLevel() >= KITKAT
&& RuntimeEnvironment.getApiLevel() < P) {
// Map from to underlying location provider storage API to location mode
- return Shadow.directlyOn(
- Settings.Secure.class,
- "getLocationModeForUser",
- ClassParameter.from(ContentResolver.class, cr),
- ClassParameter.from(int.class, 0));
+ return reflector(SettingsSecureReflector.class).getLocationModeForUser(cr, 0);
}
return get(Integer.class, name).orElse(def);
@@ -576,4 +569,10 @@ public class ShadowSettings {
public static void reset() {
canDrawOverlays = false;
}
+
+ @ForType(Settings.Secure.class)
+ interface SettingsSecureReflector {
+ @Static
+ int getLocationModeForUser(ContentResolver cr, int userId);
+ }
}
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 ff81de5db..529aaa405 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
@@ -6,6 +6,8 @@ import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O_MR1;
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.TIRAMISU;
import android.os.Build.VERSION;
import android.telephony.SubscriptionInfo;
@@ -32,11 +34,20 @@ public class ShadowSubscriptionManager {
public static final int INVALID_PHONE_INDEX =
ReflectionHelpers.getStaticField(SubscriptionManager.class, "INVALID_PHONE_INDEX");
+ private static int activeDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private static int defaultSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private static int defaultDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private static int defaultSmsSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private static int defaultVoiceSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ private final Map<Integer, String> phoneNumberMap = new HashMap<>();
+
+ /** Returns value set with {@link #setActiveDataSubscriptionId(int)}. */
+ @Implementation(minSdk = R)
+ protected static int getActiveDataSubscriptionId() {
+ return activeDataSubscriptionId;
+ }
+
/** Returns value set with {@link #setDefaultSubscriptionId(int)}. */
@Implementation(minSdk = N)
protected static int getDefaultSubscriptionId() {
@@ -85,6 +96,11 @@ public class ShadowSubscriptionManager {
return defaultDataSubscriptionId;
}
+ /** Sets the value that will be returned by {@link #getActiveDataSubscriptionId()}. */
+ public static void setActiveDataSubscriptionId(int activeDataSubscriptionId) {
+ ShadowSubscriptionManager.activeDataSubscriptionId = activeDataSubscriptionId;
+ }
+
/** Sets the value that will be returned by {@link #getDefaultSubscriptionId()}. */
public static void setDefaultSubscriptionId(int defaultSubscriptionId) {
ShadowSubscriptionManager.defaultSubscriptionId = defaultSubscriptionId;
@@ -109,8 +125,8 @@ public class ShadowSubscriptionManager {
private static Map<Integer, Integer> phoneIds = new HashMap<>();
/**
- * Cache of {@link SubscriptionInfo} used by {@link #getActiveSubscriptionInfoList}.
- * Managed by {@link #setActiveSubscriptionInfoList}.
+ * Cache of {@link SubscriptionInfo} used by {@link #getActiveSubscriptionInfoList}. Managed by
+ * {@link #setActiveSubscriptionInfoList}.
*/
private List<SubscriptionInfo> subscriptionList = new ArrayList<>();
/**
@@ -212,6 +228,7 @@ public class ShadowSubscriptionManager {
/**
* Sets the active list of {@link SubscriptionInfo}. This call internally triggers {@link
* OnSubscriptionsChangedListener#onSubscriptionsChanged()} to all the listeners.
+ *
* @param list - The subscription info list, can be null.
*/
public void setActiveSubscriptionInfoList(List<SubscriptionInfo> list) {
@@ -299,7 +316,7 @@ public class ShadowSubscriptionManager {
}
/** Clears the local cache of roaming subscription Ids used by {@link #isNetworkRoaming}. */
- public void clearNetworkRoamingStatus(){
+ public void clearNetworkRoamingStatus() {
roamingSimSubscriptionIds.clear();
}
@@ -377,8 +394,25 @@ public class ShadowSubscriptionManager {
}
}
+ /**
+ * Returns the phone number for the given {@code subscriptionId}, or an empty string if not
+ * available.
+ *
+ * <p>The phone number can be set by {@link #setPhoneNumber(int, String)}
+ */
+ @Implementation(minSdk = TIRAMISU)
+ protected String getPhoneNumber(int subscriptionId) {
+ return phoneNumberMap.getOrDefault(subscriptionId, "");
+ }
+
+ /** Sets the phone number returned by {@link #getPhoneNumber(int)}. */
+ public void setPhoneNumber(int subscriptionId, String phoneNumber) {
+ phoneNumberMap.put(subscriptionId, phoneNumber);
+ }
+
@Resetter
public static void reset() {
+ activeDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
defaultDataSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
defaultSmsSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
defaultVoiceSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
index 840c626fb..cce1990a0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
@@ -162,30 +162,30 @@ public class ShadowSystemVibrator extends ShadowVibrator {
private void recordVibratePredefined(long milliseconds, int effectId) {
vibrating = true;
- this.effectId = effectId;
- this.milliseconds = milliseconds;
+ ShadowVibrator.effectId = effectId;
+ ShadowVibrator.milliseconds = milliseconds;
handler.removeCallbacks(stopVibratingRunnable);
- handler.postDelayed(stopVibratingRunnable, this.milliseconds);
+ handler.postDelayed(stopVibratingRunnable, ShadowVibrator.milliseconds);
}
private void recordVibrate(long milliseconds) {
vibrating = true;
- this.milliseconds = milliseconds;
+ ShadowVibrator.milliseconds = milliseconds;
handler.removeCallbacks(stopVibratingRunnable);
- handler.postDelayed(stopVibratingRunnable, this.milliseconds);
+ handler.postDelayed(stopVibratingRunnable, ShadowVibrator.milliseconds);
}
protected void recordVibratePattern(long[] pattern, int repeat) {
vibrating = true;
- this.pattern = pattern;
- this.repeat = repeat;
+ ShadowVibrator.pattern = pattern;
+ ShadowVibrator.repeat = repeat;
handler.removeCallbacks(stopVibratingRunnable);
if (repeat < 0) {
long endDelayMillis = 0;
for (long t : pattern) {
endDelayMillis += t;
}
- this.milliseconds = endDelayMillis;
+ ShadowVibrator.milliseconds = endDelayMillis;
handler.postDelayed(stopVibratingRunnable, endDelayMillis);
}
}
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 e7499eafc..dc456f3f6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -131,6 +131,7 @@ public class ShadowTelephonyManager {
private int carrierIdFromSimMccMnc;
private String subscriberId;
private /*UiccSlotInfo[]*/ Object uiccSlotInfos;
+ private /*UiccCardInfo[]*/ Object uiccCardsInfo;
private String visualVoicemailPackageName = null;
private SignalStrength signalStrength;
private boolean dataEnabled = false;
@@ -163,7 +164,8 @@ public class ShadowTelephonyManager {
callComposerStatus = 0;
}
- public static void setCallComposerStatus(int callComposerStatus) {
+ @Implementation(minSdk = S)
+ protected void setCallComposerStatus(int callComposerStatus) {
ShadowTelephonyManager.callComposerStatus = callComposerStatus;
}
@@ -487,6 +489,18 @@ public class ShadowTelephonyManager {
return uiccSlotInfos;
}
+ /** Sets the UICC cards information returned by {@link #getUiccCardsInfo()}. */
+ public void setUiccCardsInfo(/*UiccCardsInfo[]*/ Object uiccCardsInfo) {
+ this.uiccCardsInfo = uiccCardsInfo;
+ }
+
+ /** Returns the UICC cards information set by {@link #setUiccCardsInfo}. */
+ @Implementation(minSdk = Q)
+ @HiddenApi
+ protected /*UiccSlotInfo[]*/ Object getUiccCardsInfo() {
+ return uiccCardsInfo;
+ }
+
/** Clears {@code slotIndex} to state mapping and resets to default state. */
public void resetSimStates() {
simStates.clear();
@@ -898,6 +912,7 @@ public class ShadowTelephonyManager {
*/
@Implementation(minSdk = O)
protected TelephonyManager createForPhoneAccountHandle(PhoneAccountHandle handle) {
+ checkReadPhoneStatePermission();
return phoneAccountToTelephonyManagers.get(handle);
}
@@ -1075,6 +1090,9 @@ public class ShadowTelephonyManager {
*/
@Implementation(minSdk = Build.VERSION_CODES.Q)
protected boolean isEmergencyNumber(String number) {
+ if (ShadowServiceManager.getService(Context.TELEPHONY_SERVICE) == null) {
+ throw new IllegalStateException("telephony service is null.");
+ }
if (number == null) {
return false;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java
index 58abb5844..f9af04905 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTypeface.java
@@ -1,262 +1,22 @@
package org.robolectric.shadows;
-import static android.os.Build.VERSION_CODES.KITKAT;
-import static android.os.Build.VERSION_CODES.LOLLIPOP;
-import static android.os.Build.VERSION_CODES.N_MR1;
-import static android.os.Build.VERSION_CODES.O;
-import static android.os.Build.VERSION_CODES.O_MR1;
-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 org.robolectric.RuntimeEnvironment.getApiLevel;
-import static org.robolectric.Shadows.shadowOf;
-
-import android.annotation.SuppressLint;
-import android.content.res.AssetManager;
-import android.graphics.FontFamily;
import android.graphics.Typeface;
-import android.util.ArrayMap;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicLong;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.HiddenApi;
-import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.RealObject;
-import org.robolectric.annotation.Resetter;
-import org.robolectric.res.Fs;
-import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
-
-@Implements(value = Typeface.class, looseSignatures = true)
-@SuppressLint("NewApi")
-public class ShadowTypeface {
- private static final Map<Long, FontDesc> FONTS = Collections.synchronizedMap(new HashMap<>());
- private static final AtomicLong nextFontId = new AtomicLong(1);
- private FontDesc description;
-
- @HiddenApi
- @Implementation(maxSdk = KITKAT)
- protected void __constructor__(int fontId) {
- description = findById((long) fontId);
- }
-
- @HiddenApi
- @Implementation(minSdk = LOLLIPOP)
- protected void __constructor__(long fontId) {
- description = findById(fontId);
- }
-
- @Implementation
- protected static void __staticInitializer__() {
- Shadow.directInitialize(Typeface.class);
- if (RuntimeEnvironment.getApiLevel() > R) {
- Typeface.loadPreinstalledSystemFontMap();
- }
- }
-
- @Implementation(minSdk = P)
- protected static Typeface create(Typeface family, int weight, boolean italic) {
- if (family == null) {
- return createUnderlyingTypeface(null, weight);
- } else {
- ShadowTypeface shadowTypeface = Shadow.extract(family);
- return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), weight);
- }
- }
-
- @Implementation
- protected static Typeface create(String familyName, int style) {
- return createUnderlyingTypeface(familyName, style);
- }
-
- @Implementation
- protected static Typeface create(Typeface family, int style) {
- if (family == null) {
- return createUnderlyingTypeface(null, style);
- } else {
- ShadowTypeface shadowTypeface = Shadow.extract(family);
- return createUnderlyingTypeface(shadowTypeface.getFontDescription().getFamilyName(), style);
- }
- }
-
- @Implementation
- protected static Typeface createFromAsset(AssetManager mgr, String path) {
- ShadowAssetManager shadowAssetManager = Shadow.extract(mgr);
- Collection<Path> assetDirs = shadowAssetManager.getAllAssetDirs();
- for (Path assetDir : assetDirs) {
- Path assetFile = assetDir.resolve(path);
- if (Files.exists(assetFile)) {
- return createUnderlyingTypeface(path, Typeface.NORMAL);
- }
-
- // maybe path is e.g. "myFont", but we should match "myFont.ttf" too?
- Path[] files;
- try {
- files = Fs.listFiles(assetDir, f -> f.getFileName().toString().startsWith(path));
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- if (files.length != 0) {
- return createUnderlyingTypeface(path, Typeface.NORMAL);
- }
- }
-
- throw new RuntimeException("Font asset not found " + path);
- }
-
- @Implementation(minSdk = O, maxSdk = P)
- protected static Typeface createFromResources(AssetManager mgr, String path, int cookie) {
- return createUnderlyingTypeface(path, Typeface.NORMAL);
- }
-
- @Implementation(minSdk = O)
- protected static Typeface createFromResources(
- Object /* FamilyResourceEntry */ entry,
- Object /* AssetManager */ mgr,
- Object /* String */ path) {
- return createUnderlyingTypeface((String) path, Typeface.NORMAL);
- }
-
- @Implementation
- protected static Typeface createFromFile(File path) {
- String familyName = path.toPath().getFileName().toString();
- return createUnderlyingTypeface(familyName, Typeface.NORMAL);
- }
-
- @Implementation
- protected static Typeface createFromFile(String path) {
- return createFromFile(new File(path));
- }
-
- @Implementation
- protected int getStyle() {
- return description.getStyle();
- }
-
- @Override
- @Implementation
- public boolean equals(Object o) {
- if (o instanceof Typeface) {
- Typeface other = ((Typeface) o);
- return Objects.equals(getFontDescription(), shadowOf(other).getFontDescription());
- }
- return false;
- }
-
- @Override
- @Implementation
- public int hashCode() {
- return getFontDescription().hashCode();
- }
-
- @HiddenApi
- @Implementation(minSdk = LOLLIPOP)
- protected static Typeface createFromFamilies(Object /*FontFamily[]*/ families) {
- return null;
- }
-
- @HiddenApi
- @Implementation(minSdk = LOLLIPOP, maxSdk = N_MR1)
- protected static Typeface createFromFamiliesWithDefault(Object /*FontFamily[]*/ families) {
- return null;
- }
-
- @Implementation(minSdk = O, maxSdk = O_MR1)
- protected static Typeface createFromFamiliesWithDefault(
- Object /*FontFamily[]*/ families, Object /* int */ weight, Object /* int */ italic) {
- return createUnderlyingTypeface("fake-font", Typeface.NORMAL);
- }
-
- @Implementation(minSdk = P)
- protected static Typeface createFromFamiliesWithDefault(
- Object /*FontFamily[]*/ families,
- Object /* String */ fallbackName,
- Object /* int */ weight,
- Object /* int */ italic) {
- return createUnderlyingTypeface((String) fallbackName, Typeface.NORMAL);
- }
-
- @Implementation(minSdk = P, maxSdk = P)
- protected static void buildSystemFallback(
- String xmlPath,
- String fontDir,
- ArrayMap<String, Typeface> fontMap,
- ArrayMap<String, FontFamily[]> fallbackMap) {
- fontMap.put("sans-serif", createUnderlyingTypeface("sans-serif", 0));
- }
-
- /** Avoid spurious error message about /system/etc/fonts.xml */
- @Implementation(minSdk = LOLLIPOP, maxSdk = O_MR1)
- protected static void init() {}
+import org.robolectric.shadow.api.ShadowPicker;
+import org.robolectric.shadows.ShadowTypeface.Picker;
- @HiddenApi
- @Implementation(minSdk = Q, maxSdk = R)
- public static void initSystemDefaultTypefaces(
- Object systemFontMap, Object fallbacks, Object aliases) {}
-
- @Resetter
- public static synchronized void reset() {
- FONTS.clear();
- }
-
- protected static Typeface createUnderlyingTypeface(String familyName, int style) {
- long thisFontId = nextFontId.getAndIncrement();
- FONTS.put(thisFontId, new FontDesc(familyName, style));
- if (getApiLevel() >= LOLLIPOP) {
- return ReflectionHelpers.callConstructor(
- Typeface.class, ClassParameter.from(long.class, thisFontId));
- } else {
- return ReflectionHelpers.callConstructor(
- Typeface.class, ClassParameter.from(int.class, (int) thisFontId));
- }
- }
-
- private static synchronized FontDesc findById(long fontId) {
- if (FONTS.containsKey(fontId)) {
- return FONTS.get(fontId);
- }
- throw new RuntimeException("Unknown font id: " + fontId);
- }
-
- @Implementation(minSdk = O, maxSdk = R)
- protected static long nativeCreateFromArray(long[] familyArray, int weight, int italic) {
- // TODO: implement this properly
- long thisFontId = nextFontId.getAndIncrement();
- FONTS.put(thisFontId, new FontDesc(null, weight));
- return thisFontId;
- }
+/** Base class for {@link ShadowTypeface} classes. */
+@Implements(value = Typeface.class, shadowPicker = Picker.class)
+public abstract class ShadowTypeface {
/**
* Returns the font description.
*
* @return Font description.
*/
- public FontDesc getFontDescription() {
- return description;
- }
-
- @Implementation(minSdk = S)
- protected static void nativeForceSetStaticFinalField(String fieldname, Typeface typeface) {
- ReflectionHelpers.setStaticField(Typeface.class, fieldname, typeface);
- }
-
- @Implementation(minSdk = S)
- protected static long nativeCreateFromArray(
- long[] familyArray, long fallbackTypeface, int weight, int italic) {
- return ShadowTypeface.nativeCreateFromArray(familyArray, weight, italic);
- }
+ public abstract FontDesc getFontDescription();
+ /** Contains data about a font. */
public static class FontDesc {
public final String familyName;
public final int style;
@@ -305,15 +65,11 @@ public class ShadowTypeface {
}
}
- /** Shadow for {@link Typeface.Builder} */
- @Implements(value = Typeface.Builder.class, minSdk = Q)
- public static class ShadowBuilder {
- @RealObject Typeface.Builder realBuilder;
-
- @Implementation
- protected Typeface build() {
- String path = ReflectionHelpers.getField(realBuilder, "mPath");
- return createUnderlyingTypeface(path, Typeface.NORMAL);
+ /** A {@link ShadowPicker} that always selects the legacy ShadowTypeface. */
+ public static class Picker implements ShadowPicker<ShadowTypeface> {
+ @Override
+ public Class<? extends ShadowTypeface> pickShadowClass() {
+ return ShadowLegacyTypeface.class;
}
}
}
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 5a177082e..2c283d6f7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
@@ -6,7 +6,6 @@ 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.N_MR1;
-import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
@@ -855,7 +854,7 @@ public class ShadowUserManager {
* <p>This method checks whether the user handle corresponds to a managed profile, and then query
* its state. When quiet, the user is not running.
*/
- @Implementation(minSdk = O)
+ @Implementation(minSdk = N)
protected boolean isQuietModeEnabled(UserHandle userHandle) {
// Return false if this is not a managed profile (this is the OS's behavior).
if (!isManagedProfileWithoutPermission(userHandle)) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
index 3159bf923..b66a0a414 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
@@ -14,31 +14,32 @@ import java.util.Objects;
import javax.annotation.Nullable;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
@Implements(Vibrator.class)
public class ShadowVibrator {
- boolean vibrating;
- boolean cancelled;
- long milliseconds;
- protected long[] pattern;
- protected final List<VibrationEffectSegment> vibrationEffectSegments = new ArrayList<>();
- protected final List<PrimitiveEffect> primitiveEffects = new ArrayList<>();
- protected final List<Integer> supportedPrimitives = new ArrayList<>();
- @Nullable protected VibrationAttributes vibrationAttributesFromLastVibration;
- @Nullable protected AudioAttributes audioAttributesFromLastVibration;
- int repeat;
- boolean hasVibrator = true;
- boolean hasAmplitudeControl = false;
- int effectId;
+ static boolean vibrating;
+ static boolean cancelled;
+ static long milliseconds;
+ protected static long[] pattern;
+ protected static final List<VibrationEffectSegment> vibrationEffectSegments = new ArrayList<>();
+ protected static final List<PrimitiveEffect> primitiveEffects = new ArrayList<>();
+ protected static final List<Integer> supportedPrimitives = new ArrayList<>();
+ @Nullable protected static VibrationAttributes vibrationAttributesFromLastVibration;
+ @Nullable protected static AudioAttributes audioAttributesFromLastVibration;
+ static int repeat;
+ static boolean hasVibrator = true;
+ static boolean hasAmplitudeControl = false;
+ static int effectId;
/** Controls the return value of {@link Vibrator#hasVibrator()} the default is true. */
public void setHasVibrator(boolean hasVibrator) {
- this.hasVibrator = hasVibrator;
+ ShadowVibrator.hasVibrator = hasVibrator;
}
/** Controls the return value of {@link Vibrator#hasAmplitudeControl()} the default is false. */
public void setHasAmplitudeControl(boolean hasAmplitudeControl) {
- this.hasAmplitudeControl = hasAmplitudeControl;
+ ShadowVibrator.hasAmplitudeControl = hasAmplitudeControl;
}
/**
@@ -119,6 +120,23 @@ public class ShadowVibrator {
return audioAttributesFromLastVibration;
}
+ @Resetter
+ public static void reset() {
+ vibrating = false;
+ cancelled = false;
+ milliseconds = 0;
+ pattern = null;
+ vibrationEffectSegments.clear();
+ primitiveEffects.clear();
+ supportedPrimitives.clear();
+ vibrationAttributesFromLastVibration = null;
+ audioAttributesFromLastVibration = null;
+ repeat = 0;
+ hasVibrator = true;
+ hasAmplitudeControl = false;
+ effectId = 0;
+ }
+
/**
* A data class for exposing {@link VibrationEffect.Composition$PrimitiveEffect}, which is a
* hidden non TestApi class introduced in Android R.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
index 921d278c1..cfe2bc7b6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewRootImpl.java
@@ -21,9 +21,11 @@ import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewRootImpl;
+import android.view.WindowInsets;
import android.view.WindowManager;
import android.window.ClientWindowFrames;
import java.util.ArrayList;
+import java.util.Optional;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -47,6 +49,49 @@ public class ShadowViewRootImpl {
@RealObject protected ViewRootImpl realObject;
+ /**
+ * The visibility of the system status bar.
+ *
+ * <p>The value will be read in the intercepted {@link #getWindowInsets(boolean)} method providing
+ * the current state via the returned {@link WindowInsets} instance if it has been set..
+ *
+ * <p>NOTE: This state does not reflect the current state of system UI visibility flags or the
+ * current window insets. Rather it tracks the latest known state provided via {@link
+ * #setIsStatusBarVisible(boolean)}.
+ */
+ private static Optional<Boolean> isStatusBarVisible = Optional.empty();
+
+ /**
+ * The visibility of the system navigation bar.
+ *
+ * <p>The value will be read in the intercepted {@link #getWindowInsets(boolean)} method providing
+ * the current state via the returned {@link WindowInsets} instance if it has been set.
+ *
+ * <p>NOTE: This state does not reflect the current state of system UI visibility flags or the
+ * current window insets. Rather it tracks the latest known state provided via {@link
+ * #setIsNavigationBarVisible(boolean)}.
+ */
+ private static Optional<Boolean> isNavigationBarVisible = Optional.empty();
+
+ /** Allows other shadows to set the state of {@link #isStatusBarVisible}. */
+ protected static void setIsStatusBarVisible(boolean isStatusBarVisible) {
+ ShadowViewRootImpl.isStatusBarVisible = Optional.of(isStatusBarVisible);
+ }
+
+ /** Clears the last known state of {@link #isStatusBarVisible}. */
+ protected static void clearIsStatusBarVisible() {
+ ShadowViewRootImpl.isStatusBarVisible = Optional.empty();
+ }
+
+ /** Allows other shadows to set the state of {@link #isNavigationBarVisible}. */
+ protected static void setIsNavigationBarVisible(boolean isNavigationBarVisible) {
+ ShadowViewRootImpl.isNavigationBarVisible = Optional.of(isNavigationBarVisible);
+ }
+
+ /** Clears the last known state of {@link #isNavigationBarVisible}. */
+ protected static void clearIsNavigationBarVisible() {
+ ShadowViewRootImpl.isNavigationBarVisible = Optional.empty();
+ }
@Implementation(maxSdk = VERSION_CODES.JELLY_BEAN)
protected static IWindowSession getWindowSession(Looper mainLooper) {
@@ -185,6 +230,38 @@ public class ShadowViewRootImpl {
}
}
+ /**
+ * On Android R+ {@link WindowInsets} supports checking visibility of specific inset types.
+ *
+ * <p>For those SDK levels, override the real {@link WindowInsets} with the tracked system bar
+ * visibility status ({@link #isStatusBarVisible}/{@link #isNavigationBarVisible}), if set.
+ *
+ * <p>NOTE: We use state tracking in place of a longer term solution of implementing the insets
+ * calculations and broadcast (via listeners) for now. Once we have insets calculations working we
+ * should remove this mechanism.
+ */
+ @Implementation(minSdk = R)
+ protected WindowInsets getWindowInsets(boolean forceConstruct) {
+ WindowInsets realInsets =
+ reflector(ViewRootImplReflector.class, realObject).getWindowInsets(forceConstruct);
+
+ WindowInsets.Builder overridenInsetsBuilder = new WindowInsets.Builder(realInsets);
+
+ if (isStatusBarVisible.isPresent()) {
+ overridenInsetsBuilder =
+ overridenInsetsBuilder.setVisible(
+ WindowInsets.Type.statusBars(), isStatusBarVisible.get());
+ }
+
+ if (isNavigationBarVisible.isPresent()) {
+ overridenInsetsBuilder =
+ overridenInsetsBuilder.setVisible(
+ WindowInsets.Type.navigationBars(), isNavigationBarVisible.get());
+ }
+
+ return overridenInsetsBuilder.build();
+ }
+
@Resetter
public static void reset() {
ViewRootImplReflector viewRootImplStatic = reflector(ViewRootImplReflector.class);
@@ -192,6 +269,9 @@ public class ShadowViewRootImpl {
viewRootImplStatic.setFirstDrawHandlers(new ArrayList<>());
viewRootImplStatic.setFirstDrawComplete(false);
viewRootImplStatic.setConfigCallbacks(new ArrayList<>());
+
+ clearIsStatusBarVisible();
+ clearIsNavigationBarVisible();
}
public void callWindowFocusChanged(boolean hasFocus) {
@@ -389,5 +469,9 @@ public class ShadowViewRootImpl {
// SDK >= T
void windowFocusChanged(boolean hasFocus);
+
+ // SDK >= M
+ @Direct
+ WindowInsets getWindowInsets(boolean forceConstruct);
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java
index 87cb0f27e..648b44988 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWallpaperManager.java
@@ -1,5 +1,9 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
import android.Manifest.permission;
import android.annotation.FloatRange;
import android.annotation.RequiresPermission;
@@ -61,12 +65,12 @@ public class ShadowWallpaperManager {
* <p>This only caches the resource id in memory. Calling this will override any previously set
* resource and does not differentiate between users.
*/
- @Implementation(maxSdk = VERSION_CODES.M)
+ @Implementation(maxSdk = M)
protected void setResource(int resid) {
setResource(resid, WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK);
}
- @Implementation(minSdk = VERSION_CODES.N)
+ @Implementation(minSdk = N)
protected int setResource(int resid, int which) {
if ((which & (WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK)) == 0) {
return 0;
@@ -100,18 +104,21 @@ public class ShadowWallpaperManager {
* @param which either {@link WallpaperManager#FLAG_LOCK} or {WallpaperManager#FLAG_SYSTEM}
* @return 0 if fails to cache. Otherwise, 1.
*/
- @Implementation(minSdk = VERSION_CODES.P)
+ @Implementation(minSdk = N)
protected int setBitmap(Bitmap fullImage, Rect visibleCropHint, boolean allowBackup, int which) {
- if (which == WallpaperManager.FLAG_LOCK) {
+ if ((which & (WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK)) == 0) {
+ return 0;
+ }
+ if ((which & WallpaperManager.FLAG_LOCK) == WallpaperManager.FLAG_LOCK) {
lockScreenImage = fullImage;
wallpaperInfo = null;
- return 1;
- } else if (which == WallpaperManager.FLAG_SYSTEM) {
+ }
+
+ if ((which & WallpaperManager.FLAG_SYSTEM) == WallpaperManager.FLAG_SYSTEM) {
homeScreenImage = fullImage;
wallpaperInfo = null;
- return 1;
}
- return 0;
+ return 1;
}
/**
@@ -138,7 +145,7 @@ public class ShadowWallpaperManager {
* @return An open, readable file descriptor to the requested wallpaper image file; {@code null}
* if no such wallpaper is configured.
*/
- @Implementation(minSdk = VERSION_CODES.P)
+ @Implementation(minSdk = N)
@Nullable
protected ParcelFileDescriptor getWallpaperFile(int which) {
if (which == WallpaperManager.FLAG_SYSTEM && homeScreenImage != null) {
@@ -149,7 +156,7 @@ public class ShadowWallpaperManager {
return null;
}
- @Implementation(minSdk = VERSION_CODES.N)
+ @Implementation(minSdk = N)
protected boolean isSetWallpaperAllowed() {
return isWallpaperAllowed;
}
@@ -158,7 +165,7 @@ public class ShadowWallpaperManager {
isWallpaperAllowed = allowed;
}
- @Implementation(minSdk = VERSION_CODES.M)
+ @Implementation(minSdk = M)
protected boolean isWallpaperSupported() {
return isWallpaperSupported;
}
@@ -176,17 +183,20 @@ public class ShadowWallpaperManager {
* @param which either {@link WallpaperManager#FLAG_LOCK} or {WallpaperManager#FLAG_SYSTEM}
* @return 0 if fails to cache. Otherwise, 1.
*/
- @Implementation(minSdk = VERSION_CODES.N)
+ @Implementation(minSdk = N)
protected int setStream(
InputStream bitmapData, Rect visibleCropHint, boolean allowBackup, int which) {
- if (which == WallpaperManager.FLAG_LOCK) {
+ if ((which & (WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK)) == 0) {
+ return 0;
+ }
+ if ((which & WallpaperManager.FLAG_LOCK) == WallpaperManager.FLAG_LOCK) {
lockScreenImage = BitmapFactory.decodeStream(bitmapData);
- return 1;
- } else if (which == WallpaperManager.FLAG_SYSTEM) {
+ }
+
+ if ((which & WallpaperManager.FLAG_SYSTEM) == WallpaperManager.FLAG_SYSTEM) {
homeScreenImage = BitmapFactory.decodeStream(bitmapData);
- return 1;
}
- return 0;
+ return 1;
}
/**
@@ -196,7 +206,7 @@ public class ShadowWallpaperManager {
* previously set static wallpaper.
*/
@SystemApi
- @Implementation(minSdk = VERSION_CODES.M)
+ @Implementation(minSdk = M)
@RequiresPermission(permission.SET_WALLPAPER_COMPONENT)
protected boolean setWallpaperComponent(ComponentName wallpaperService)
throws IOException, XmlPullParserException {
@@ -222,17 +232,17 @@ public class ShadowWallpaperManager {
* Returns the information about the wallpaper if the current wallpaper is a live wallpaper
* component. Otherwise, if the wallpaper is a static image, this returns null.
*/
- @Implementation(minSdk = VERSION_CODES.M)
+ @Implementation
protected WallpaperInfo getWallpaperInfo() {
return wallpaperInfo;
}
- @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ @Implementation(minSdk = TIRAMISU)
protected void setWallpaperDimAmount(@FloatRange(from = 0f, to = 1f) float dimAmount) {
wallpaperDimAmount = MathUtils.saturate(dimAmount);
}
- @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+ @Implementation(minSdk = TIRAMISU)
@FloatRange(from = 0f, to = 1f)
protected float getWallpaperDimAmount() {
return wallpaperDimAmount;
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 83ca1fb3c..561ed907f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
@@ -65,6 +65,7 @@ public class ShadowWifiManager {
private final ConcurrentHashMap<WifiManager.OnWifiUsabilityStatsListener, Executor>
wifiUsabilityStatsListeners = new ConcurrentHashMap<>();
private final List<WifiUsabilityScore> usabilityScores = new ArrayList<>();
+ private Object networkScorer;
@RealObject WifiManager wifiManager;
private WifiConfiguration apConfig;
@@ -436,6 +437,32 @@ public class ShadowWifiManager {
}
}
+ /**
+ * Implements setWifiConnectedNetworkScorer() with the generic Object input as
+ * WifiConnectedNetworkScorer is a hidden/System API.
+ */
+ @Implementation(minSdk = R)
+ @HiddenApi
+ protected boolean setWifiConnectedNetworkScorer(Object executorObject, Object scorerObject) {
+ if (networkScorer == null) {
+ networkScorer = scorerObject;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Implementation(minSdk = R)
+ @HiddenApi
+ protected void clearWifiConnectedNetworkScorer() {
+ networkScorer = null;
+ }
+
+ /** Returns if wifi connected betwork scorer enabled */
+ public boolean isWifiConnectedNetworkScorerEnabled() {
+ return networkScorer != null;
+ }
+
@Implementation
protected boolean setWifiApConfiguration(WifiConfiguration apConfig) {
this.apConfig = apConfig;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
index 80b484cdc..75b0371eb 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWindowManagerGlobal.java
@@ -2,40 +2,25 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
-import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
-import static android.os.Build.VERSION_CODES.M;
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.S_V2;
import static org.robolectric.shadows.ShadowView.useRealGraphics;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.app.Instrumentation;
import android.content.ClipData;
import android.content.Context;
-import android.graphics.Rect;
import android.os.Binder;
-import android.os.Build.VERSION;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.view.DisplayCutout;
-import android.view.IWindow;
import android.view.IWindowManager;
import android.view.IWindowSession;
-import android.view.InputChannel;
-import android.view.InsetsSourceControl;
-import android.view.InsetsState;
-import android.view.InsetsVisibilities;
-import android.view.Surface;
-import android.view.SurfaceControl;
import android.view.View;
-import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import androidx.annotation.Nullable;
+import java.lang.reflect.Proxy;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -53,44 +38,18 @@ import org.robolectric.util.reflector.Static;
minSdk = JELLY_BEAN_MR1,
looseSignatures = true)
public class ShadowWindowManagerGlobal {
- private static WindowSessionDelegate windowSessionDelegate;
+ private static WindowSessionDelegate windowSessionDelegate = new WindowSessionDelegate();
private static IWindowSession windowSession;
@Resetter
public static void reset() {
reflector(WindowManagerGlobalReflector.class).setDefaultWindowManager(null);
- windowSessionDelegate = null;
+ windowSessionDelegate = new WindowSessionDelegate();
windowSession = null;
}
- private static synchronized WindowSessionDelegate getWindowSessionDelegate() {
- if (windowSessionDelegate == null) {
- int apiLevel = RuntimeEnvironment.getApiLevel();
- if (apiLevel >= S_V2) {
- windowSessionDelegate = new WindowSessionDelegateSV2();
- } else if (apiLevel >= S) {
- windowSessionDelegate = new WindowSessionDelegateS();
- } else if (apiLevel >= R) {
- windowSessionDelegate = new WindowSessionDelegateR();
- } else if (apiLevel >= Q) {
- windowSessionDelegate = new WindowSessionDelegateQ();
- } else if (apiLevel >= P) {
- windowSessionDelegate = new WindowSessionDelegateP();
- } else if (apiLevel >= M) {
- windowSessionDelegate = new WindowSessionDelegateM();
- } else if (apiLevel >= LOLLIPOP_MR1) {
- windowSessionDelegate = new WindowSessionDelegateLMR1();
- } else if (apiLevel >= JELLY_BEAN_MR1) {
- windowSessionDelegate = new WindowSessionDelegateJBMR1();
- } else {
- windowSessionDelegate = new WindowSessionDelegateJB();
- }
- }
- return windowSessionDelegate;
- }
-
public static boolean getInTouchMode() {
- return getWindowSessionDelegate().getInTouchMode();
+ return windowSessionDelegate.getInTouchMode();
}
/**
@@ -98,7 +57,7 @@ public class ShadowWindowManagerGlobal {
* Instrumentation#setInTouchMode(boolean)} to modify this from a test.
*/
static void setInTouchMode(boolean inTouchMode) {
- getWindowSessionDelegate().setInTouchMode(inTouchMode);
+ windowSessionDelegate.setInTouchMode(inTouchMode);
}
/**
@@ -107,21 +66,46 @@ public class ShadowWindowManagerGlobal {
*/
@Nullable
public static ClipData getLastDragClipData() {
- return windowSessionDelegate != null ? windowSessionDelegate.lastDragClipData : null;
+ return windowSessionDelegate.lastDragClipData;
}
/** Clears the data returned by {@link #getLastDragClipData()}. */
public static void clearLastDragClipData() {
- if (windowSessionDelegate != null) {
- windowSessionDelegate.lastDragClipData = null;
- }
+ windowSessionDelegate.lastDragClipData = null;
}
@Implementation(minSdk = JELLY_BEAN_MR2)
protected static synchronized IWindowSession getWindowSession() {
if (windowSession == null) {
+ // Use Proxy.newProxyInstance instead of ReflectionHelpers.createDelegatingProxy as there are
+ // too many variants of 'add', 'addToDisplay', and 'addToDisplayAsUser', some of which have
+ // arg types that don't exist any more.
windowSession =
- ReflectionHelpers.createDelegatingProxy(IWindowSession.class, getWindowSessionDelegate());
+ (IWindowSession)
+ Proxy.newProxyInstance(
+ IWindowSession.class.getClassLoader(),
+ new Class<?>[] {IWindowSession.class},
+ (proxy, method, args) -> {
+ String methodName = method.getName();
+ switch (methodName) {
+ case "add": // SDK 16
+ case "addToDisplay": // SDK 17-29
+ case "addToDisplayAsUser": // SDK 30+
+ return windowSessionDelegate.getAddFlags();
+ case "getInTouchMode":
+ return windowSessionDelegate.getInTouchMode();
+ case "performDrag":
+ return windowSessionDelegate.performDrag(args);
+ case "prepareDrag":
+ return windowSessionDelegate.prepareDrag();
+ case "setInTouchMode":
+ windowSessionDelegate.setInTouchMode((boolean) args[0]);
+ return null;
+ default:
+ return ReflectionHelpers.defaultValueForType(
+ method.getReturnType().getName());
+ }
+ });
}
return windowSession;
}
@@ -143,7 +127,7 @@ public class ShadowWindowManagerGlobal {
if (service == null) {
service = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE));
reflector(WindowManagerGlobalReflector.class).setWindowManagerService(service);
- if (VERSION.SDK_INT >= 30) {
+ if (RuntimeEnvironment.getApiLevel() >= R) {
reflector(WindowManagerGlobalReflector.class).setUseBlastAdapter(service.useBLAST());
}
}
@@ -169,7 +153,7 @@ public class ShadowWindowManagerGlobal {
void setUseBlastAdapter(boolean useBlastAdapter);
}
- private abstract static class WindowSessionDelegate {
+ private static class WindowSessionDelegate {
// From WindowManagerGlobal (was WindowManagerImpl in JB).
static final int ADD_FLAG_IN_TOUCH_MODE = 0x1;
static final int ADD_FLAG_APP_VISIBLE = 0x2;
@@ -202,200 +186,20 @@ public class ShadowWindowManagerGlobal {
this.inTouchMode = inTouchMode;
}
- // @Implementation(maxSdk = O_MR1)
- public IBinder prepareDrag(
- IWindow window, int flags, int thumbnailWidth, int thumbnailHeight, Surface outSurface) {
- return new Binder();
- }
-
- // @Implementation(maxSdk = M)
- public boolean performDrag(
- IWindow window,
- IBinder dragToken,
- float touchX,
- float touchY,
- float thumbCenterX,
- float thumbCenterY,
- ClipData data) {
- lastDragClipData = data;
- return true;
- }
-
- // @Implementation(minSdk = N, maxSdk = O_MR1)
- public boolean performDrag(
- IWindow window,
- IBinder dragToken,
- int touchSource,
- float touchX,
- float touchY,
- float thumbCenterX,
- float thumbCenterY,
- ClipData data) {
- lastDragClipData = data;
- return true;
- }
- }
-
- private static class WindowSessionDelegateJB extends WindowSessionDelegate {
- // @Implementation(maxSdk = JELLY_BEAN)
- public int add(
- IWindow window,
- int seq,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int layerStackId,
- Rect outContentInsets,
- InputChannel outInputChannel) {
- return getAddFlags();
- }
- }
-
- private static class WindowSessionDelegateJBMR1 extends WindowSessionDelegateJB {
- // @Implementation(minSdk = JELLY_BEAN_MR1, maxSdk = LOLLIPOP)
- public int addToDisplay(
- IWindow window,
- int seq,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int layerStackId,
- Rect outContentInsets,
- InputChannel outInputChannel) {
- return getAddFlags();
- }
- }
-
- private static class WindowSessionDelegateLMR1 extends WindowSessionDelegateJBMR1 {
- // @Implementation(sdk = LOLLIPOP_MR1)
- public int addToDisplay(
- IWindow window,
- int seq,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int layerStackId,
- Rect outContentInsets,
- Rect outStableInsets,
- InputChannel outInputChannel) {
- return getAddFlags();
- }
- }
-
- private static class WindowSessionDelegateM extends WindowSessionDelegateLMR1 {
- // @Implementation(minSdk = M, maxSdk = O_MR1)
- public int addToDisplay(
- IWindow window,
- int seq,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int layerStackId,
- Rect outContentInsets,
- Rect outStableInsets,
- Rect outInsets,
- InputChannel outInputChannel) {
- return getAddFlags();
- }
- }
-
- private static class WindowSessionDelegateP extends WindowSessionDelegateM {
- // @Implementation(sdk = P)
- public int addToDisplay(
- IWindow window,
- int seq,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int layerStackId,
- Rect outFrame,
- Rect outContentInsets,
- Rect outStableInsets,
- Rect outOutsets,
- DisplayCutout.ParcelableWrapper displayCutout,
- InputChannel outInputChannel) {
- return getAddFlags();
- }
-
- // @Implementation(minSdk = P)
- public IBinder performDrag(
- IWindow window,
- int flags,
- SurfaceControl surface,
- int touchSource,
- float touchX,
- float touchY,
- float thumbCenterX,
- float thumbCenterY,
- ClipData data) {
- lastDragClipData = data;
+ public IBinder prepareDrag() {
return new Binder();
}
- }
- private static class WindowSessionDelegateQ extends WindowSessionDelegateP {
- // @Implementation(sdk = Q)
- public int addToDisplay(
- IWindow window,
- int seq,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int layerStackId,
- Rect outFrame,
- Rect outContentInsets,
- Rect outStableInsets,
- Rect outOutsets,
- DisplayCutout.ParcelableWrapper displayCutout,
- InputChannel outInputChannel,
- InsetsState insetsState) {
- return getAddFlags();
- }
- }
-
- private static class WindowSessionDelegateR extends WindowSessionDelegateQ {
- // @Implementation(sdk = R)
- public int addToDisplayAsUser(
- IWindow window,
- int seq,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int layerStackId,
- int userId,
- Rect outFrame,
- Rect outContentInsets,
- Rect outStableInsets,
- DisplayCutout.ParcelableWrapper displayCutout,
- InputChannel outInputChannel,
- InsetsState insetsState,
- InsetsSourceControl[] activeControls) {
- return getAddFlags();
- }
- }
-
- private static class WindowSessionDelegateS extends WindowSessionDelegateR {
- // @Implementation(sdk = S)
- public int addToDisplayAsUser(
- IWindow window,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int layerStackId,
- int userId,
- InsetsState requestedVisibility,
- InputChannel outInputChannel,
- InsetsState insetsState,
- InsetsSourceControl[] activeControls) {
- return getAddFlags();
- }
- }
-
- private static class WindowSessionDelegateSV2 extends WindowSessionDelegateS {
- // @Implementation(minSdk = S_V2)
- public int addToDisplayAsUser(
- IWindow window,
- WindowManager.LayoutParams attrs,
- int viewVisibility,
- int displayId,
- int userId,
- InsetsVisibilities requestedVisibilities,
- InputChannel outInputChannel,
- InsetsState outInsetsState,
- InsetsSourceControl[] outActiveControls) {
- return getAddFlags();
+ public Object performDrag(Object[] args) {
+ // extract the clipData param
+ for (int i = args.length - 1; i >= 0; i--) {
+ if (args[i] instanceof ClipData) {
+ lastDragClipData = (ClipData) args[i];
+ // In P (SDK 28), the return type changed from boolean to Binder.
+ return RuntimeEnvironment.getApiLevel() >= P ? new Binder() : true;
+ }
+ }
+ throw new AssertionError("Missing ClipData param");
}
}
}