diff options
author | Rex Hoffman <rexhoffman@google.com> | 2022-12-17 00:07:04 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-12-17 00:07:04 +0000 |
commit | afca64d5446704036f0c8f0657d086440bd6b91c (patch) | |
tree | 3508a823b93a48eaaceabdcea6d1e3dcd62c5a00 | |
parent | 183707e3c7e551130df7fefafe6cacd6c320c179 (diff) | |
parent | 58bd83a4a472f6d6cef6ad0ce2e61f1350d084e1 (diff) | |
download | robolectric-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>
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"); } } } |