aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-04-14 14:44:21 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-04-14 14:44:21 +0000
commitf11789c7532131b05ba5d7c86ee5b432f49ba099 (patch)
tree2aba65167e81ebf211f1503a64fa3f5ccc08c537
parent70ae854c1a5a6d161490f4f6a700ea24cbea3fae (diff)
parent669bd7c87aad3fc72259b08e79aeacf34e9ebced (diff)
downloadrobolectric-android13-qpr3-c-s8-release.tar.gz
Change-Id: Iaf1372b35245ad3745bae61ebfafc7624856de14
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md (renamed from .github/ISSUE_TEMPLATE.md)9
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md20
-rw-r--r--.github/workflows/build_native_runtime.yml144
-rw-r--r--.github/workflows/check_code_formatting.yml6
-rw-r--r--.github/workflows/gradle_tasks_validation.yml11
-rw-r--r--.github/workflows/gradle_wrapper_validation.yml6
-rw-r--r--.github/workflows/graphics_tests.yml47
-rw-r--r--.github/workflows/tests.yml51
-rw-r--r--README.md5
-rw-r--r--annotations/src/main/java/org/robolectric/annotation/Config.java28
-rw-r--r--build.gradle31
-rw-r--r--buildSrc/build.gradle6
-rw-r--r--dependencies.gradle20
-rw-r--r--errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java2
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--integration_tests/androidx_test/build.gradle2
-rw-r--r--integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java32
-rw-r--r--integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java33
-rw-r--r--integration_tests/compat-target28/build.gradle2
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/content/pm/PackageManagerTest.java34
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/content/res/AssetManagerTest.java13
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/text/format/DateFormatTest.java33
-rw-r--r--integration_tests/ctesque/src/sharedTest/java/android/webkit/CookieManagerTest.java10
-rw-r--r--integration_tests/jacoco-offline/build.gradle92
-rw-r--r--integration_tests/jacoco-offline/src/main/java/org/robolectric/integrationtests/jacoco/JaCoCoTester.java10
-rw-r--r--integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/JaCoCoTesterTest.java15
-rw-r--r--integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/ShadowJaCoCoTester.java15
-rw-r--r--integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/ShadowJaCoCoTesterTest.java21
-rw-r--r--integration_tests/kotlin/build.gradle28
-rw-r--r--integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/CustomShadowImageView.kt21
-rw-r--r--integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/CustomShadowImageViewTest.kt25
-rw-r--r--integration_tests/libphonenumber/build.gradle2
-rw-r--r--integration_tests/memoryleaks/build.gradle2
-rw-r--r--integration_tests/mockito-kotlin/build.gradle2
-rw-r--r--integration_tests/mockk/build.gradle4
-rw-r--r--integration_tests/nativegraphics/AndroidManifest.xml9
-rw-r--r--integration_tests/nativegraphics/build.gradle40
-rw-r--r--integration_tests/nativegraphics/src/main/AndroidManifest.xml9
-rw-r--r--integration_tests/nativegraphics/src/main/assets/almost-red-adobe.pngbin0 -> 351 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/blue-16bit-prophoto.pngbin0 -> 655 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/blue-16bit-srgb.pngbin0 -> 938 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/StaticLayoutLineBreakingTestFont.ttfbin0 -> 1044 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_g3em_weight400_upright.ttfbin0 -> 1824 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_h3em_weight400_italic.ttfbin0 -> 1824 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_m3em_weight700_upright.ttfbin0 -> 1824 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/layout/linebreak.ttfbin0 -> 1940 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/layout/samplefont.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont2.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont3.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/samplefont.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/bombfont.ttfbin0 -> 812 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/bombfont2.ttfbin0 -> 812 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/out_of_unicode_end_cmap12.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/out_of_unicode_start_cmap12.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/too_large_end_cmap12.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/too_large_start_cmap12.ttfbin0 -> 1768 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap12.ttfbin0 -> 1828 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap14_default_uvs.ttfbin0 -> 1884 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap14_non_default_uvs.ttfbin0 -> 1904 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap4.ttfbin0 -> 1824 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/grayscale-16bit-linearSrgb.pngbin0 -> 789 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/grayscale-linearSrgb.pngbin0 -> 262 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/green-p3.pngbin0 -> 842 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/green-srgb.pngbin0 -> 964 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/purple-cmyk.pngbin0 -> 869 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/purple-displayprofile.pngbin0 -> 5671 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/red-adobergb.pngbin0 -> 1041 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/assets/translucent-green-p3.pngbin0 -> 993 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/anim/animation_grouping_1_01.xml26
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/colors.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_clamp.xml30
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item.xml32
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_overlap.xml33
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_overlap_mirror.xml34
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_repeat.xml33
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial.xml27
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_clamp.xml28
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item.xml30
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_repeat.xml31
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_short.xml26
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_short_mirror.xml27
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep.xml27
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_clamp.xml28
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item.xml30
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_long.xml30
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_long_mirror.xml31
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_repeat.xml31
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/stroke_gradient.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/stroke_gradient_clamp.xml30
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item.xml32
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_alpha.xml28
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_alpha_mirror.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_repeat.xml33
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/styles.xml198
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/vector_icon_fill_state_list.xml22
-rw-r--r--integration_tests/nativegraphics/src/main/res/color/vector_icon_stroke_state_list.xml22
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/alpha_mask.pngbin0 -> 901 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/black1.pngbin0 -> 3974 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/blackitalic1.pngbin0 -> 4995 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/blue_padded_square.9.pngbin0 -> 213 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/bold1.pngbin0 -> 3977 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/bolditalic1.pngbin0 -> 4844 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensed1.pngbin0 -> 3780 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedbold1.pngbin0 -> 3771 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedbolditalic1.pngbin0 -> 4727 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/condenseditalic1.pngbin0 -> 4796 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedlight1.pngbin0 -> 3727 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedlightitalic1.pngbin0 -> 4832 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/extrabold1.pngbin0 -> 3998 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/extrabolditalic1.pngbin0 -> 4784 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_blue_circle.pngbin0 -> 941 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_dashed_oval.pngbin0 -> 1180 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_scaled.pngbin0 -> 729 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_subset.pngbin0 -> 956 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_transformed.pngbin0 -> 971 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_ninepatch.pngbin0 -> 334 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_headless_robot.pngbin0 -> 610 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_robot.pngbin0 -> 1273 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/hello1.pngbin0 -> 4110 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/index_8.pngbin0 -> 160 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/italic1.pngbin0 -> 5058 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/light1.pngbin0 -> 4142 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/lightitalic1.pngbin0 -> 4993 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/medium1.pngbin0 -> 4139 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/mediumitalic1.pngbin0 -> 5076 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/padding_0.9.pngbin0 -> 4057 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathclippingtest_torus.pngbin0 -> 740 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_circle.pngbin0 -> 1944 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_cubics.pngbin0 -> 2014 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_quads.pngbin0 -> 1956 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_rect.pngbin0 -> 381 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/robot.pngbin0 -> 9397 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/text_on_path.pngbin0 -> 3996 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/text_path_with_offset.pngbin0 -> 965 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/thin1.pngbin0 -> 3405 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/thinitalic1.pngbin0 -> 4026 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_drawable_scale_golden.pngbin0 -> 534 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_arcto_golden.pngbin0 -> 1119 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_clip_path_1_golden.pngbin0 -> 654 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_create_golden.pngbin0 -> 387 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_delete_golden.pngbin0 -> 369 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_filltype_evenodd_golden.pngbin0 -> 2149 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_filltype_nonzero_golden.pngbin0 -> 2070 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_1_clamp_golden.pngbin0 -> 1778 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_1_golden.pngbin0 -> 2280 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_2_golden.pngbin0 -> 2111 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_2_repeat_golden.pngbin0 -> 2170 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_3_golden.pngbin0 -> 2145 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_3_mirror_golden.pngbin0 -> 2315 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_group_clip_golden.pngbin0 -> 347 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_heart_golden.pngbin0 -> 584 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_implicit_lineto_golden.pngbin0 -> 191 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_random_path_1_golden.pngbin0 -> 2334 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_random_path_2_golden.pngbin0 -> 2740 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_render_order_1_golden.pngbin0 -> 360 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_render_order_2_golden.pngbin0 -> 342 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_a_1_golden.pngbin0 -> 3786 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_a_2_golden.pngbin0 -> 3763 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_cq_golden.pngbin0 -> 2317 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_st_golden.pngbin0 -> 3190 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_1_golden.pngbin0 -> 535 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_2_golden.pngbin0 -> 421 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_3_golden.pngbin0 -> 481 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_schedule_golden.pngbin0 -> 1261 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_settings_golden.pngbin0 -> 822 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_2_golden.pngbin0 -> 922 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_2_pressed_golden.pngbin0 -> 921 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_golden.pngbin0 -> 922 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_pressed_golden.pngbin0 -> 921 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_1_golden.pngbin0 -> 622 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_2_golden.pngbin0 -> 561 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_3_golden.pngbin0 -> 538 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_1_golden.pngbin0 -> 665 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_2_golden.pngbin0 -> 299 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_3_golden.pngbin0 -> 324 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_4_golden.pngbin0 -> 745 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_5_golden.pngbin0 -> 909 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_6_golden.pngbin0 -> 778 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/alpha.pngbin0 -> 323 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/animated.gifbin0 -> 34978 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/animation_vector_drawable_grouping_1.xml26
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/baseline_jpeg.jpgbin0 -> 200258 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/bitmap_density.xml19
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/bitmap_shader_am_density.xml22
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/bitmap_shader_density.xml21
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/bmp_test.bmpbin0 -> 230454 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/circle.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/dashed_oval.xml25
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/gif_test.gifbin0 -> 43199 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/google_logo_1.pngbin0 -> 26279 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/google_logo_2.webpbin0 -> 10800 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/grayscale_jpg.jpgbin0 -> 770 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/icon_black.jpgbin0 -> 42111 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/icon_blue.jpgbin0 -> 7977 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/icon_blue_png.pngbin0 -> 133 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/ninepatch_0.9.pngbin0 -> 119 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/ninepatchdrawable.xml22
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/pass.jpgbin0 -> 14169 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/png_test.pngbin0 -> 426765 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/premul_data.pngbin0 -> 154 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/rectangle.xml28
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/robot.pngbin0 -> 5634 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/start.jpgbin0 -> 14163 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/sunset1.jpgbin0 -> 25011 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_density.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_drawable_grouping_1.xml56
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_arcto.xml24
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_clip_path_1.xml79
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_create.xml33
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_delete.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_filltype_evenodd.xml33
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_filltype_nonzero.xml33
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_1.xml91
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_1_clamp.xml91
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_2.xml90
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_2_repeat.xml90
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_3.xml90
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_3_mirror.xml90
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_group_clip.xml50
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_heart.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_implicit_lineto.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_random_path_1.xml49
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_random_path_2.xml49
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_render_order_1.xml89
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_render_order_2.xml89
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_a_1.xml43
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_a_2.xml45
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_cq.xml42
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_st.xml42
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_1.xml52
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_2.xml48
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_3.xml45
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_schedule.xml32
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_settings.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_state_list.xml31
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_state_list_2.xml31
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_1.xml46
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_2.xml46
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_3.xml46
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_1.xml38
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_2.xml48
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_3.xml48
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_4.xml68
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_5.xml81
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_6.xml85
-rw-r--r--integration_tests/nativegraphics/src/main/res/drawable/webp_test.webpbin0 -> 22538 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/font/a3em.ttfbin0 -> 1836 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/font/b3em.ttfbin0 -> 1784 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/font/c3em.ttfbin0 -> 1784 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/font/multistyle_family.xml6
-rw-r--r--integration_tests/nativegraphics/src/main/res/font/multiweight_family.xml6
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/blue_padded_layout.xml28
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/blue_padded_square.xml19
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/circle_clipped_webview.xml25
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/force_dark_siblings.xml30
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/frame_layout.xml19
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/frame_layout_webview.xml21
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/simple_force_dark.xml20
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/simple_rect_layout.xml24
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/simple_red_layout.xml19
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/simple_shadow_layout.xml27
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/simple_white_layout.xml20
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/skew_layout.xml26
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/test_container.xml24
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/test_content_webview.xml19
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/textureview.xml19
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/vector_drawable_scale_layout.xml36
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/viewpropertyanimator_test_layout.xml33
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/webview_canvas_rrect_clip.xml19
-rw-r--r--integration_tests/nativegraphics/src/main/res/layout/wide_gamut_bitmap_layout.xml19
-rw-r--r--integration_tests/nativegraphics/src/main/res/raw/sample_1mp.dngbin0 -> 87116 bytes
-rw-r--r--integration_tests/nativegraphics/src/main/res/values-television/dimens.xml18
-rw-r--r--integration_tests/nativegraphics/src/main/res/values/arrays.xml57
-rw-r--r--integration_tests/nativegraphics/src/main/res/values/attrs.xml146
-rw-r--r--integration_tests/nativegraphics/src/main/res/values/colors.xml29
-rw-r--r--integration_tests/nativegraphics/src/main/res/values/dimens.xml21
-rw-r--r--integration_tests/nativegraphics/src/main/res/values/strings.xml184
-rw-r--r--integration_tests/nativegraphics/src/main/res/values/styles.xml198
-rw-r--r--integration_tests/nativegraphics/src/main/res/values/themes.xml34
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/BitmapLoadingTest.java37
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/BitmapUtils.java282
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ColorUtils.java153
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/DrawableTestUtils.java306
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/FontLoadingTest.java27
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/HardwareAcceleratedActivityRenderTest.java29
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/MediaStoreTest.java28
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ScrollViewTest.java36
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAllocationRegistryTest.java24
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedImageDrawableTest.java36
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java74
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapDrawableTest.java50
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapFactoryTest.java561
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapShaderTest.java150
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapTest.java1763
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBlendModeColorFilterTest.java96
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBlurMaskFilterTest.java69
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeCanvasTest.java77
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorFilterTest.java82
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorMatrixColorFilterTest.java114
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorSpaceTest.java912
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorTest.java80
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeComposePathEffectTest.java70
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeComposeShaderTest.java176
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeCornerPathEffectTest.java95
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeDashPathEffectTest.java102
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeDiscretePathEffectTest.java96
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeEmbossMaskFilterTest.java83
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeFontFamilyTest.java129
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeFontTest.java35
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeHardwareRendererObserverTest.java36
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeHardwareRendererTest.java27
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeImageDecoderTest.java301
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeInterpolatorTest.java230
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLightingColorFilterTest.java92
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLineBreakerTest.java116
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLinearGradientTest.java301
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMaskFilterTest.java18
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMatrixTest.java445
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMeasuredParagraphTest.java198
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMeasuredTextTest.java123
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeNativeInterpolatorFactoryTest.java28
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePaintTest.java2202
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathDashPathEffectTest.java74
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathEffectTest.java19
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathMeasureTest.java154
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathParserTest.java85
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathTest.java453
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePictureTest.java153
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePorterDuffColorFilterTest.java77
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePropertyValuesHolderTest.java55
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRadialGradientTest.java341
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRegionIteratorTest.java44
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRegionTest.java1531
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRenderNodeAnimatorTest.java25
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRenderNodeTest.java277
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRuntimeShaderTest.java81
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeShaderTest.java86
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeStaticLayoutTest.java1940
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeSumPathEffectTest.java62
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeSweepGradientTest.java387
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTableMaskFilterTest.java46
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeThreadedRendererTest.java21
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTypefaceTest.java354
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeVectorDrawableTest.java511
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/BitmapComparer.java37
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/ExactComparer.java51
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/MSSIMComparer.java182
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/BitmapVerifier.java49
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/BlurPixelVerifier.java109
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/ColorCountVerifier.java52
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/ColorVerifier.java43
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/GoldenImageVerifier.java62
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/PerPixelBitmapVerifier.java95
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/RectVerifier.java41
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/RegionVerifier.java76
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/SamplePointWideGamutVerifier.java84
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/differencevisualizers/DifferenceVisualizer.java21
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/differencevisualizers/PassFailVisualizer.java40
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/text/EditorState.java186
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/AssertionError.kt25
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/CompareUtils.java31
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/DrawCountDown.java53
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/SneakyThrow.java40
-rw-r--r--integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/WebViewReadyHelper.java61
-rw-r--r--integration_tests/play_services/build.gradle3
-rw-r--r--integration_tests/security-providers/build.gradle2
-rw-r--r--integration_tests/sparsearray/build.gradle2
-rw-r--r--junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java5
-rw-r--r--nativeruntime/build.gradle198
-rw-r--r--nativeruntime/cpp/CMakeLists.txt186
-rw-r--r--nativeruntime/cpp/androidfw/CMakeLists.txt11
-rw-r--r--nativeruntime/cpp/androidfw/CursorWindow.cpp431
-rw-r--r--nativeruntime/cpp/androidfw/include/androidfw/CursorWindow.h215
-rw-r--r--nativeruntime/cpp/base/include/android-base/macros.h155
-rw-r--r--nativeruntime/cpp/jni/AndroidRuntime.cpp47
-rw-r--r--nativeruntime/cpp/jni/AndroidRuntime.h41
-rw-r--r--nativeruntime/cpp/jni/JNIMain.cpp62
-rw-r--r--nativeruntime/cpp/jni/robo_android_database_CursorWindow.cpp628
-rw-r--r--nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.cpp251
-rw-r--r--nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.h54
-rw-r--r--nativeruntime/cpp/jni/robo_android_database_SQLiteConnection.cpp1068
-rw-r--r--nativeruntime/cpp/libcutils/CMakeLists.txt8
-rw-r--r--nativeruntime/cpp/libcutils/ashmem.cpp98
-rw-r--r--nativeruntime/cpp/libcutils/include/cutils/ashmem.h37
-rw-r--r--nativeruntime/cpp/liblog/CMakeLists.txt5
-rw-r--r--nativeruntime/cpp/liblog/include/log/log.h78
-rw-r--r--nativeruntime/cpp/liblog/log.c35
-rw-r--r--nativeruntime/cpp/libnativehelper/include/nativehelper/JNIHelp.h552
-rw-r--r--nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_local_ref.h85
-rw-r--r--nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_utf8_chars.h93
-rw-r--r--nativeruntime/cpp/libutils/CMakeLists.txt17
-rw-r--r--nativeruntime/cpp/libutils/SharedBuffer.cpp135
-rw-r--r--nativeruntime/cpp/libutils/SharedBuffer.h145
-rw-r--r--nativeruntime/cpp/libutils/String16.cpp450
-rw-r--r--nativeruntime/cpp/libutils/String8.cpp575
-rw-r--r--nativeruntime/cpp/libutils/Unicode.cpp561
-rw-r--r--nativeruntime/cpp/libutils/include/utils/Compat.h104
-rw-r--r--nativeruntime/cpp/libutils/include/utils/Errors.h93
-rw-r--r--nativeruntime/cpp/libutils/include/utils/String16.h331
-rw-r--r--nativeruntime/cpp/libutils/include/utils/String8.h371
-rw-r--r--nativeruntime/cpp/libutils/include/utils/TypeHelpers.h370
-rw-r--r--nativeruntime/cpp/libutils/include/utils/Unicode.h170
m---------nativeruntime/external/icu0
m---------nativeruntime/external/sqlite0
-rw-r--r--plugins/maven-dependency-resolver/build.gradle48
-rw-r--r--plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java86
-rw-r--r--plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.kt83
-rw-r--r--plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java291
-rw-r--r--plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.kt294
-rw-r--r--processor/build.gradle4
-rw-r--r--resources/build.gradle2
-rw-r--r--resources/src/main/java/org/robolectric/res/android/Chunk.java19
-rw-r--r--resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java11
-rw-r--r--resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java17
-rw-r--r--resources/src/main/java/org/robolectric/res/android/LoadedArsc.java110
-rw-r--r--resources/src/main/java/org/robolectric/res/android/ResourceTypes.java58
-rw-r--r--resources/src/test/java/org/robolectric/res/android/LoadedArscTest.java63
-rw-r--r--robolectric/build.gradle23
-rw-r--r--robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java8
-rwxr-xr-xrobolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java5
-rw-r--r--robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java7
-rw-r--r--robolectric/src/main/java/org/robolectric/internal/SandboxManager.java18
-rw-r--r--robolectric/src/main/resources/robolectric-version.properties1
-rw-r--r--robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java17
-rw-r--r--robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java32
-rw-r--r--robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java46
-rw-r--r--robolectric/src/test/java/org/robolectric/plugins/GraphicsModeTransitionTest.java52
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/AudioDeviceInfoBuilderTest.java25
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java22
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java40
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java7
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowGeocoderTest.java25
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowHardwareBufferTest.java3
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyLooperTest.java4
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowLinuxTest.java61
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowLocationManagerTest.java131
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowLooperResetterTest.java191
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java3
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java80
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java12
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java28
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowViewGroupTest.java83
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java33
-rw-r--r--robolectric/src/test/java/org/robolectric/shadows/SharedLibraryInfoBuilderTest.java49
-rw-r--r--robolectric/src/test/resources/res/menu/action_menu.xml8
-rw-r--r--sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java4
-rw-r--r--settings.gradle4
-rw-r--r--shadows/framework/build.gradle6
-rw-r--r--shadows/framework/src/main/java/android/webkit/RoboCookieManager.java17
-rw-r--r--shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java30
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java68
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java7
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java24
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java12
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager10.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager9.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java22
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowGeocoder.java26
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java10
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobScheduler.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyAssetManager.java3
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLinux.java17
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocationManager.java296
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java36
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowMemoryMappedFileS.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java1
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java6
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java8
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedSystemClock.java19
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java16
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java19
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java39
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java27
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/SharedLibraryInfoBuilder.java125
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java41
-rw-r--r--shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java2
-rw-r--r--utils/build.gradle6
483 files changed, 25592 insertions, 8496 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/bug_report.md
index c8dc466f7..a35216124 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,3 +1,12 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
### Description
### Steps to Reproduce
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..bbcbbe7d6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/build_native_runtime.yml b/.github/workflows/build_native_runtime.yml
deleted file mode 100644
index 0ef105ea2..000000000
--- a/.github/workflows/build_native_runtime.yml
+++ /dev/null
@@ -1,144 +0,0 @@
-name: Build Native Runtime
-
-on:
- push:
- branches: [ master ]
-
- pull_request:
- branches: [ master, google ]
-
-permissions:
- contents: read
-
-jobs:
- build_native_runtime:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ ubuntu-20.04, macos-latest, self-hosted, windows-2019 ]
- steps:
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
- submodules: recursive
-
- # msys2 only log not fail on non-Windows platform.
- - uses: msys2/setup-msys2@v2
- with:
- msystem: mingw64
- update: true
- platform-check-severity: warn
- location: D:\
- install: >-
- base-devel
- mingw-w64-x86_64-toolchain
- mingw-w64-x86_64-cmake
- mingw-w64-x86_64-ninja
-
- - name: Set up JDK 11
- uses: actions/setup-java@v3
- with:
- distribution: 'zulu'
- java-version: 11
-
- - name: Install build tools (Mac OS)
- if: runner.os == 'macOS'
- run: brew install ninja
-
- - name: Install build tools (Linux)
- if: runner.os == 'Linux'
- run: sudo apt-get install ninja-build
-
- - name: Cache ICU build output (Non Windows)
- id: cache-icu
- uses: actions/cache@v3
- if: runner.os == 'Linux' || runner.os == 'macOS'
- with:
- path: ~/icu-bin
- key: ${{ runner.os }}-${{ runner.arch }}-icu-${{ hashFiles('nativeruntime/external/icu/**') }}
-
- - name: Cache ICU build output (Windows)
- id: cache-icu-windows
- uses: actions/cache@v3
- if: runner.os == 'Windows'
- with:
- path: D:\msys64\home\runneradmin\icu-bin
- key: ${{ runner.os }}-${{ runner.arch }}-icu-windows-${{ hashFiles('nativeruntime/external/icu/**') }}
-
- - name: Build ICU for MacOS
- if: runner.os =='macOS' && steps.cache-icu.outputs.cache-hit != 'true'
- run: |
- cd nativeruntime/external/icu/icu4c/source
- ./runConfigureICU MacOSX --enable-static --prefix=$HOME/icu-bin
- make -j4
- make install
-
- - name: Build ICU for Linux
- if: runner.os == 'Linux' && steps.cache-icu.outputs.cache-hit != 'true'
- run: |
- export CFLAGS="$CFLAGS -fPIC"
- export CXXFLAGS="$CXXFLAGS -fPIC"
- cd nativeruntime/external/icu/icu4c/source
- ./runConfigureICU Linux --enable-static --prefix=$HOME/icu-bin
- make -j4
- make install
-
- - name: Build ICU for Windows
- if: runner.os == 'Windows' && steps.cache-icu-windows.outputs.cache-hit != 'true'
- shell: msys2 {0}
- run: |
- cd nativeruntime/external/icu/icu4c/source
- ./runConfigureICU MinGW --enable-static --prefix=$HOME/icu-bin
- make -j4
- make install
-
- - name: Run CMake (Non Windows)
- if: runner.os == 'Linux' || runner.os == 'macOS'
- run: |
- mkdir build
- cd build
- ICU_ROOT_DIR=$HOME/icu-bin cmake -B . -S ../nativeruntime/cpp -G Ninja
- ninja
-
- - name: Run CMake (Windows)
- if: runner.os == 'Windows'
- shell: msys2 {0}
- run: |
- mkdir build
- cd build
- ICU_ROOT_DIR=$HOME/icu-bin cmake -B . -S ../nativeruntime/cpp -G Ninja
- ninja
-
- - name: Rename libnativeruntime for Linux
- if: runner.os == 'Linux'
- run: |
- echo "NATIVERUNTIME_ARTIFACT_FILE=librobolectric-nativeruntime-linux-x86_64.so" >> $GITHUB_ENV
- mv build/libnativeruntime.so build/librobolectric-nativeruntime-linux-x86_64.so
-
- - name: Rename libnativeruntime for macOS
- if: runner.os == 'macOS'
- run: |
- echo "NATIVERUNTIME_ARTIFACT_FILE=librobolectric-nativeruntime-mac-$(uname -m).dylib" >> $GITHUB_ENV
- mv build/libnativeruntime.dylib build/librobolectric-nativeruntime-mac-$(uname -m).dylib
-
- - name: Rename libnativeruntime for Windows
- if: runner.os == 'Windows'
- shell: msys2 {0}
- run: |
- mv build/libnativeruntime.dll build/robolectric-nativeruntime-windows-x86_64.dll
-
- - name: Upload libnativeruntime (Non Windows)
- if: runner.os == 'Linux' || runner.os == 'macOS'
- uses: actions/upload-artifact@v3
- with:
- name: ${{env.NATIVERUNTIME_ARTIFACT_FILE}}
- path: |
- build/${{env.NATIVERUNTIME_ARTIFACT_FILE}}
-
- - name: Upload libnativeruntime (Windows)
- if: runner.os == 'Windows'
- uses: actions/upload-artifact@v3
- with:
- name: robolectric-nativeruntime-windows-x86_64.dll
- path: |
- build/robolectric-nativeruntime-windows-x86_64.dll
diff --git a/.github/workflows/check_code_formatting.yml b/.github/workflows/check_code_formatting.yml
index d3ad97bc6..e99374582 100644
--- a/.github/workflows/check_code_formatting.yml
+++ b/.github/workflows/check_code_formatting.yml
@@ -2,10 +2,14 @@ name: Check Code Formatting
on:
push:
- branches: [ master ]
+ branches: [ master, 'robolectric-*.x' ]
+ paths-ignore:
+ - '**.md'
pull_request:
branches: [ master, google ]
+ paths-ignore:
+ - '**.md'
permissions:
contents: read
diff --git a/.github/workflows/gradle_tasks_validation.yml b/.github/workflows/gradle_tasks_validation.yml
index 6a8c2fd12..96b4b3307 100644
--- a/.github/workflows/gradle_tasks_validation.yml
+++ b/.github/workflows/gradle_tasks_validation.yml
@@ -2,10 +2,14 @@ name: Gradle Tasks Validation
on:
push:
- branches: [ master ]
+ branches: [ master, 'robolectric-*.x' ]
+ paths-ignore:
+ - '**.md'
pull_request:
branches: [ master, google ]
+ paths-ignore:
+ - '**.md'
permissions:
contents: read
@@ -16,9 +20,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- with:
- fetch-depth: 0
- submodules: recursive
- name: Set up JDK
uses: actions/setup-java@v3
@@ -52,4 +53,4 @@ jobs:
run: SKIP_NATIVERUNTIME_BUILD=true ./gradlew :preinstrumented:instrumentAll
- name: Run :preinstrumented:instrumentAll with SDK 33
- run: SKIP_NATIVERUNTIME_BUILD=true PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll \ No newline at end of file
+ run: SKIP_NATIVERUNTIME_BUILD=true PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll
diff --git a/.github/workflows/gradle_wrapper_validation.yml b/.github/workflows/gradle_wrapper_validation.yml
index 03a37e580..b7e100892 100644
--- a/.github/workflows/gradle_wrapper_validation.yml
+++ b/.github/workflows/gradle_wrapper_validation.yml
@@ -2,10 +2,14 @@ name: Validate Gradle Wrapper
on:
push:
- branches: [ master ]
+ branches: [ master, 'robolectric-*.x' ]
+ paths-ignore:
+ - '**.md'
pull_request:
branches: [ master, google ]
+ paths-ignore:
+ - '**.md'
permissions:
contents: read
diff --git a/.github/workflows/graphics_tests.yml b/.github/workflows/graphics_tests.yml
new file mode 100644
index 000000000..e0d385a16
--- /dev/null
+++ b/.github/workflows/graphics_tests.yml
@@ -0,0 +1,47 @@
+name: Graphics Tests
+
+on:
+ push:
+ branches: [ master, 'robolectric-*.x' ]
+ paths-ignore:
+ - '**.md'
+
+ pull_request:
+ branches: [ master, google ]
+ paths-ignore:
+ - '**.md'
+
+permissions:
+ contents: read
+
+jobs:
+ graphics_tests:
+ runs-on: self-hosted
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu'
+ java-version: 11
+
+ - uses: gradle/gradle-build-action@v2
+
+ - name: Run unit tests
+ run: |
+ SKIP_ERRORPRONE=true SKIP_JAVADOC=true ./gradlew :integration_tests:nativegraphics:testDebugUnitTest \
+ --info --stacktrace --continue \
+ --parallel \
+ --no-watch-fs \
+ -Drobolectric.alwaysIncludeVariantMarkersInTestName=true \
+ -Dorg.gradle.workers.max=2
+
+ - name: Upload Test Results
+ uses: actions/upload-artifact@v3
+ if: always()
+ with:
+ name: test_results
+ path: '**/build/test-results/**/TEST-*.xml'
+
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index c6d887ee4..d83e804a7 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,10 +2,14 @@ name: Tests
on:
push:
- branches: [ master ]
+ branches: [ master, 'robolectric-*.x' ]
+ paths-ignore:
+ - '**.md'
pull_request:
branches: [ master, google ]
+ paths-ignore:
+ - '**.md'
permissions:
contents: read
@@ -19,8 +23,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- with:
- submodules: recursive
- name: Set up JDK 11
uses: actions/setup-java@v3
@@ -30,27 +32,9 @@ jobs:
- uses: gradle/gradle-build-action@v2
- - name: Cache ICU build output
- id: cache-icu
- uses: actions/cache@v3
- with:
- path: ~/icu-bin
- key: ${{ runner.os }}-${{ runner.arch }}-icu-${{ hashFiles('nativeruntime/external/icu/**') }}
-
- - name: Build ICU
- if: steps.cache-icu.outputs.cache-hit != 'true'
- run: |
- cd nativeruntime/external/icu/icu4c/source
- CFLAGS="-fPIC" CXXFLAGS="-fPIC" ./runConfigureICU Linux --enable-static --prefix=$HOME/icu-bin
- make -j4
- make install
-
- - name: Install Ninja
- run: sudo apt-get install ninja-build
-
- name: Build
run: |
- ICU_ROOT_DIR=$HOME/icu-bin SKIP_ERRORPRONE=true SKIP_JAVADOC=true \
+ SKIP_ERRORPRONE=true SKIP_JAVADOC=true \
./gradlew clean assemble testClasses --parallel --stacktrace --no-watch-fs
unit-tests:
@@ -63,8 +47,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- with:
- submodules: recursive
- name: Set up JDK 11
uses: actions/setup-java@v3
@@ -74,25 +56,16 @@ jobs:
- uses: gradle/gradle-build-action@v2
- - name: Cache ICU build output
- id: cache-icu
- uses: actions/cache@v3
- with:
- path: ~/icu-bin
- key: ${{ runner.os }}-${{ runner.arch }}-icu-${{ hashFiles('nativeruntime/external/icu/**') }}
-
- - name: Install Ninja
- run: sudo apt-get install ninja-build
-
- name: Run unit tests
run: |
- ICU_ROOT_DIR=$HOME/icu-bin SKIP_ERRORPRONE=true SKIP_JAVADOC=true ./gradlew test \
+ SKIP_ERRORPRONE=true SKIP_JAVADOC=true ./gradlew test \
--info --stacktrace --continue \
--parallel \
--no-watch-fs \
-Drobolectric.enabledSdks=${{ matrix.api-versions }} \
-Drobolectric.alwaysIncludeVariantMarkersInTestName=true \
- -Dorg.gradle.workers.max=2
+ -Dorg.gradle.workers.max=2 \
+ -x :integration_tests:nativegraphics:test
- name: Upload Test Results
uses: actions/upload-artifact@v3
@@ -110,12 +83,10 @@ jobs:
# Allow tests to continue on other devices if they fail on one device.
fail-fast: false
matrix:
- api-level: [ 29, 33 ]
+ api-level: [ 29 ]
steps:
- uses: actions/checkout@v3
- with:
- submodules: recursive
- name: Set up JDK 11
uses: actions/setup-java@v3
@@ -129,7 +100,7 @@ jobs:
id: determine-target
run: |
TARGET="google_apis"
- echo "::set-output name=TARGET::$TARGET"
+ echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
- name: AVD cache
uses: actions/cache@v3
diff --git a/README.md b/README.md
index 3ee66862e..6b91ba31d 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ If you'd like to start a new project with Robolectric tests you can refer to `de
```groovy
testImplementation "junit:junit:4.13.2"
-testImplementation "org.robolectric:robolectric:4.9.1"
+testImplementation "org.robolectric:robolectric:4.10-alpha-1"
```
## Building And Contributing
@@ -54,9 +54,6 @@ Those software configurations are recommended and tested.
- JDK 11. Gradle JVM should be set to Java 11.
- For command line, make sure the environment variable `JAVA_HOME` is correctly point to JDK11, or set the build environment by [Gradle CLI option](https://docs.gradle.org/current/userguide/command_line_interface.html#sec:environment_options) `-Dorg.gradle.java.home="YourJdkHomePath"` or by [Gradle Properties](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties) `org.gradle.java.home=YourJdkHomePath`.
- For both IntelliJ and Android Studio, see _Settings/Preferences | Build, Execution, Deployment | Build Tools | Gradle_.
-- Ninja 1.10.2+. Check it by `ninja --version`.
-- CMake 3.22.1+. Check it by `cmake --version`.
-- GCC 7.5.0+ on Linux or Apple clang 12.0.0+ on macOS. Check it by `gcc --version`.
See [Building Robolectric](http://robolectric.org/building-robolectric/) for more details about setting up a build environment for Robolectric.
diff --git a/annotations/src/main/java/org/robolectric/annotation/Config.java b/annotations/src/main/java/org/robolectric/annotation/Config.java
index fed003253..1553e6d96 100644
--- a/annotations/src/main/java/org/robolectric/annotation/Config.java
+++ b/annotations/src/main/java/org/robolectric/annotation/Config.java
@@ -33,6 +33,7 @@ public @interface Config {
String DEFAULT_VALUE_STRING = "--default";
int DEFAULT_VALUE_INT = -1;
+ float DEFAULT_FONT_SCALE = 1.0f;
String DEFAULT_MANIFEST_NAME = "AndroidManifest.xml";
Class<? extends Application> DEFAULT_APPLICATION = DefaultApplication.class;
@@ -56,6 +57,13 @@ public @interface Config {
int maxSdk() default -1;
/**
+ * The default font scale. In U+, users will have a slider to determine font scale. In all
+ * previous APIs, font scales are either small (0.85f), normal (1.0f), large (1.15f) or huge
+ * (1.3f)
+ */
+ float fontScale() default 1.0f;
+
+ /**
* The Android manifest file to load; Robolectric will look relative to the current directory.
* Resources and assets will be loaded relative to the manifest.
*
@@ -166,6 +174,7 @@ public @interface Config {
private final int[] sdk;
private final int minSdk;
private final int maxSdk;
+ private final float fontScale;
private final String manifest;
private final String qualifiers;
private final String resourceDir;
@@ -184,6 +193,7 @@ public @interface Config {
parseSdkInt(properties.getProperty("maxSdk", "-1")),
properties.getProperty("manifest", DEFAULT_VALUE_STRING),
properties.getProperty("qualifiers", DEFAULT_QUALIFIERS),
+ Float.parseFloat(properties.getProperty("fontScale", "1.0f")),
properties.getProperty("packageName", DEFAULT_PACKAGE_NAME),
properties.getProperty("resourceDir", DEFAULT_RES_FOLDER),
properties.getProperty("assetDir", DEFAULT_ASSET_FOLDER),
@@ -284,6 +294,7 @@ public @interface Config {
int maxSdk,
String manifest,
String qualifiers,
+ float fontScale,
String packageName,
String resourceDir,
String assetDir,
@@ -296,6 +307,7 @@ public @interface Config {
this.maxSdk = maxSdk;
this.manifest = manifest;
this.qualifiers = qualifiers;
+ this.fontScale = fontScale;
this.packageName = packageName;
this.resourceDir = resourceDir;
this.assetDir = assetDir;
@@ -328,6 +340,11 @@ public @interface Config {
}
@Override
+ public float fontScale() {
+ return fontScale;
+ }
+
+ @Override
public Class<? extends Application> application() {
return application;
}
@@ -413,6 +430,7 @@ public @interface Config {
protected int[] sdk = new int[0];
protected int minSdk = -1;
protected int maxSdk = -1;
+ protected float fontScale = 1.0f;
protected String manifest = Config.DEFAULT_VALUE_STRING;
protected String qualifiers = Config.DEFAULT_QUALIFIERS;
protected String packageName = Config.DEFAULT_PACKAGE_NAME;
@@ -431,6 +449,7 @@ public @interface Config {
maxSdk = config.maxSdk();
manifest = config.manifest();
qualifiers = config.qualifiers();
+ fontScale = config.fontScale();
packageName = config.packageName();
resourceDir = config.resourceDir();
assetDir = config.assetDir();
@@ -475,6 +494,11 @@ public @interface Config {
return this;
}
+ public Builder setFontScale(float fontScale) {
+ this.fontScale = fontScale;
+ return this;
+ }
+
public Builder setAssetDir(String assetDir) {
this.assetDir = assetDir;
return this;
@@ -515,6 +539,7 @@ public @interface Config {
int[] overlaySdk = overlayConfig.sdk();
int overlayMinSdk = overlayConfig.minSdk();
int overlayMaxSdk = overlayConfig.maxSdk();
+ float overlayFontScale = overlayConfig.fontScale();
//noinspection ConstantConditions
if (overlaySdk != null && overlaySdk.length > 0) {
@@ -532,6 +557,8 @@ public @interface Config {
}
this.manifest = pick(this.manifest, overlayConfig.manifest(), DEFAULT_VALUE_STRING);
+ this.fontScale = pick(this.fontScale, overlayFontScale, DEFAULT_FONT_SCALE);
+
String qualifiersOverlayValue = overlayConfig.qualifiers();
if (qualifiersOverlayValue != null && !qualifiersOverlayValue.equals("")) {
if (qualifiersOverlayValue.startsWith("+")) {
@@ -583,6 +610,7 @@ public @interface Config {
maxSdk,
manifest,
qualifiers,
+ fontScale,
packageName,
resourceDir,
assetDir,
diff --git a/build.gradle b/build.gradle
index c224f5b8c..1eb662b1f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,12 +14,11 @@ buildscript {
dependencies {
gradle
- classpath 'com.android.tools.build:gradle:7.3.1'
- classpath 'net.ltgt.gradle:gradle-errorprone-plugin:2.0.2'
+ classpath 'com.android.tools.build:gradle:7.4.2'
+ classpath 'net.ltgt.gradle:gradle-errorprone-plugin:3.0.1'
classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:3.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
- classpath "com.github.ben-manes:gradle-versions-plugin:0.42.0"
- classpath "com.diffplug.spotless:spotless-plugin-gradle:6.9.1"
+ classpath "com.diffplug.spotless:spotless-plugin-gradle:6.17.0"
}
}
@@ -35,7 +34,6 @@ allprojects {
}
apply plugin: 'idea'
-apply plugin: 'com.github.ben-manes.versions'
project.ext.configAnnotationProcessing = []
project.afterEvaluate {
@@ -122,29 +120,6 @@ task aggregateDocs {
dependsOn ':aggregateJsondocs'
}
-// aggregate test results from all projects...
-task aggregateTestReports(type: TestReport) {
- def jobNumber = System.getenv('TRAVIS_JOB_NUMBER')
- if (jobNumber == null) {
- destinationDir = file("$buildDir/reports/allTests")
- } else {
- destinationDir = file("$buildDir/reports/allTests/$jobNumber")
- }
-}
-
-afterEvaluate {
- def aggregateTestReportsTask = rootProject.tasks['aggregateTestReports']
-
- allprojects.each { p ->
- p.afterEvaluate {
- p.tasks.withType(Test) { t ->
- aggregateTestReportsTask.reportOn binResultsDir
- finalizedBy aggregateTestReportsTask
- }
- }
- }
-}
-
task prefetchSdks() {
AndroidSdk.ALL_SDKS.each { androidSdk ->
doLast {
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 082d9dbf3..612366308 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -12,7 +12,7 @@ dependencies {
implementation localGroovy()
api "com.google.guava:guava:31.1-jre"
- api 'org.jetbrains:annotations:16.0.2'
- implementation "org.ow2.asm:asm-tree:9.2"
- implementation 'com.android.tools.build:gradle:7.3.1'
+ api 'org.jetbrains:annotations:24.0.1'
+ implementation "org.ow2.asm:asm-tree:9.4"
+ implementation 'com.android.tools.build:gradle:7.4.2'
}
diff --git a/dependencies.gradle b/dependencies.gradle
index 10a586c1f..b890dc16d 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,37 +1,39 @@
ext {
- apiCompatVersion='4.9'
+ apiCompatVersion='4.9.2'
- errorproneVersion='2.16'
+ errorproneVersion='2.18.0'
errorproneJavacVersion='9+181-r4173-1'
// AndroidX test versions
- axtMonitorVersion='1.6.0'
- axtRunnerVersion='1.5.0'
+ axtMonitorVersion='1.6.1'
+ axtRunnerVersion='1.5.2'
axtRulesVersion='1.5.0'
axtCoreVersion='1.5.0'
axtTruthVersion='1.5.0'
- espressoVersion='3.5.0'
+ espressoVersion='3.5.1'
axtJunitVersion='1.1.4'
axtTestServicesVersion='1.4.2'
// AndroidX versions
coreVersion='1.9.0'
- appCompatVersion='1.4.1'
+ appCompatVersion='1.6.1'
constraintlayoutVersion='2.1.4'
windowVersion='1.0.0'
- fragmentVersion='1.5.3'
+ fragmentVersion='1.5.5'
truthVersion='1.1.3'
junitVersion='4.13.2'
- mockitoVersion='4.1.0'
+ mockitoVersion='4.11.0'
+
+ jacocoVersion='0.8.8'
guavaJREVersion='31.1-jre'
asmVersion='9.4'
- kotlinVersion='1.7.20'
+ kotlinVersion='1.8.10'
autoServiceVersion='1.0.1'
multidexVersion='2.0.1'
sqlite4javaVersion='1.0.392'
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 06c634f1c..646c78e60 100644
--- a/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java
+++ b/errorprone/src/main/java/org/robolectric/errorprone/bugpatterns/RobolectricShadow.java
@@ -44,7 +44,7 @@ import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
/**
- * Ensure Robolectric shadow's method marked with {@code @Implemenetation} is protected
+ * Ensure Robolectric shadow's method marked with {@code @Implementation} is protected
*
* @author christianw@google.com (Christian Williams)
*/
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ae04661ee..070cb702f 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/integration_tests/androidx_test/build.gradle b/integration_tests/androidx_test/build.gradle
index 11ac35de6..7f6f621b6 100644
--- a/integration_tests/androidx_test/build.gradle
+++ b/integration_tests/androidx_test/build.gradle
@@ -11,7 +11,9 @@ android {
defaultConfig {
minSdk 16
targetSdk 33
+ multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArguments useTestStorageService: 'true'
}
compileOptions {
diff --git a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java
index 41e61aca6..acbd67a5f 100644
--- a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java
+++ b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/ActivityScenarioTest.java
@@ -9,9 +9,9 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
-import androidx.appcompat.R;
-import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.R;
import androidx.lifecycle.Lifecycle.State;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
@@ -69,7 +69,7 @@ public class ActivityScenarioTest {
@Override
public void onStop() {
super.onStop();
- callbacks.add("onStop");
+ callbacks.add("onStop " + isChangingConfigurations());
}
@Override
@@ -134,10 +134,18 @@ public class ActivityScenarioTest {
assertThat(activityScenario).isNotNull();
activityScenario.moveToState(State.CREATED);
activityScenario.moveToState(State.RESUMED);
- assertThat(callbacks)
- .containsExactly(
- "onCreate", "onStart", "onPostCreate", "onResume", "onWindowFocusChanged true",
- "onPause", "onStop", "onRestart", "onStart", "onResume");
+ assertThat(callbacks)
+ .containsExactly(
+ "onCreate",
+ "onStart",
+ "onPostCreate",
+ "onResume",
+ "onWindowFocusChanged true",
+ "onPause",
+ "onStop false",
+ "onRestart",
+ "onStart",
+ "onResume");
}
@Test
@@ -220,6 +228,16 @@ public class ActivityScenarioTest {
.isNotSameInstanceAs(fragment));
}
+ @Test
+ public void recreate_isChangingConfigurations() {
+ try (ActivityScenario<TranscriptActivity> activityScenario =
+ ActivityScenario.launch(TranscriptActivity.class)) {
+ activityScenario.recreate();
+
+ assertThat(callbacks).contains("onStop true");
+ }
+ }
+
@Config(minSdk = JELLY_BEAN_MR2)
@Test
public void setRotation_recreatesActivity() {
diff --git a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java
index f70c0c952..9e5428bf3 100644
--- a/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java
+++ b/integration_tests/androidx_test/src/test/java/org/robolectric/integrationtests/axt/EspressoIdlingResourceTest.java
@@ -146,6 +146,39 @@ public final class EspressoIdlingResourceTest {
}
}
+ @SuppressWarnings("FutureReturnValueIgnored")
+ @Test
+ public void onIdle_looperPostsToMainThread_shouldWaitForTheTaskOnMainThreadToFinish()
+ throws Exception {
+ HandlerThread handlerThread = new HandlerThread("Test");
+ try {
+ handlerThread.start();
+ Handler handler = new Handler(handlerThread.getLooper());
+ CountDownLatch handlerStarted = new CountDownLatch(1);
+ CountDownLatch releaseHandler = new CountDownLatch(1);
+ AtomicBoolean mainThreadPosted = new AtomicBoolean(false);
+ handler.post(
+ () -> {
+ handlerStarted.countDown();
+ try {
+ releaseHandler.await();
+ new Handler(Looper.getMainLooper()).post(() -> mainThreadPosted.set(true));
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ });
+ handlerStarted.await();
+ idlingRegistry.registerLooperAsIdlingResource(handlerThread.getLooper());
+
+ executor.submit(releaseHandler::countDown);
+ onIdle();
+
+ assertThat(mainThreadPosted.get()).isTrue();
+ } finally {
+ handlerThread.quit();
+ }
+ }
+
private static class NamedIdleResource implements IdlingResource {
final String name;
final AtomicBoolean isIdle;
diff --git a/integration_tests/compat-target28/build.gradle b/integration_tests/compat-target28/build.gradle
index 6188f42b8..37a856856 100644
--- a/integration_tests/compat-target28/build.gradle
+++ b/integration_tests/compat-target28/build.gradle
@@ -8,7 +8,7 @@ apply plugin: "com.diffplug.spotless"
spotless {
kotlin {
target '**/*.kt'
- ktfmt('0.34').googleStyle()
+ ktfmt('0.42').googleStyle()
}
}
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/content/pm/PackageManagerTest.java b/integration_tests/ctesque/src/sharedTest/java/android/content/pm/PackageManagerTest.java
index c7f60f199..694760b57 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/content/pm/PackageManagerTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/content/pm/PackageManagerTest.java
@@ -16,7 +16,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import java.util.ArrayList;
@@ -38,7 +38,7 @@ public final class PackageManagerTest {
@Before
public void setup() throws Exception {
- context = InstrumentationRegistry.getTargetContext();
+ context = ApplicationProvider.getApplicationContext();
pm = context.getPackageManager();
}
@@ -99,7 +99,7 @@ public final class PackageManagerTest {
}
@Test
- public void getComponent_partialName() throws Exception {
+ public void getComponent_partialName() {
ComponentName serviceName = new ComponentName(context, ".TestService");
try {
@@ -110,7 +110,7 @@ public final class PackageManagerTest {
}
@Test
- public void getComponent_wrongNameActivity() throws Exception {
+ public void getComponent_wrongNameActivity() {
ComponentName activityName = new ComponentName(context, "WrongNameActivity");
try {
@@ -137,14 +137,14 @@ public final class PackageManagerTest {
}
@Test
- public void queryIntentServices_noFlags() throws Exception {
+ public void queryIntentServices_noFlags() {
List<ResolveInfo> result = pm.queryIntentServices(new Intent(context, TestService.class), 0);
assertThat(result).hasSize(1);
}
@Test
- public void getCompoent_disabledComponent_doesntInclude() throws Exception {
+ public void getComponent_disabledComponent_doesntInclude() {
ComponentName disabledActivityName =
new ComponentName(context, "org.robolectric.testapp.DisabledTestActivity");
@@ -156,7 +156,7 @@ public final class PackageManagerTest {
}
@Test
- public void getCompoent_disabledComponent_include() throws Exception {
+ public void getComponent_disabledComponent_include() throws Exception {
ComponentName disabledActivityName =
new ComponentName(context, "org.robolectric.testapp.DisabledTestActivity");
@@ -166,8 +166,7 @@ public final class PackageManagerTest {
}
@Test
- public void getPackageInfo_programmaticallyDisabledComponent_noFlags_notReturned()
- throws Exception {
+ public void getPackageInfo_programmaticallyDisabledComponent_noFlags_notReturned() {
ComponentName activityName = new ComponentName(context, "org.robolectric.testapp.TestActivity");
pm.setComponentEnabledSetting(activityName, COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
@@ -207,7 +206,7 @@ public final class PackageManagerTest {
@Test
@Config(maxSdk = 23)
@SdkSuppress(maxSdkVersion = 23)
- public void getPackageInfo_disabledAplication_stillReturned_below24() throws Exception {
+ public void getPackageInfo_disabledApplication_stillReturned_below24() throws Exception {
pm.setApplicationEnabledSetting(
context.getPackageName(), COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
@@ -232,7 +231,7 @@ public final class PackageManagerTest {
@Test
@Config(minSdk = 24)
@SdkSuppress(minSdkVersion = 24)
- public void getPackageInfo_disabledAplication_stillReturned_after24() throws Exception {
+ public void getPackageInfo_disabledApplication_stillReturned_after24() throws Exception {
pm.setApplicationEnabledSetting(
context.getPackageName(), COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
@@ -249,7 +248,7 @@ public final class PackageManagerTest {
}
@Test
- public void getPackageInfo_disabledAplication_withFlags_returnedEverything() throws Exception {
+ public void getPackageInfo_disabledApplication_withFlags_returnedEverything() throws Exception {
pm.setApplicationEnabledSetting(
context.getPackageName(), COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
@@ -267,7 +266,7 @@ public final class PackageManagerTest {
}
@Test
- public void getApplicationInfo_disabledAplication_stillReturnedWithNoFlags() throws Exception {
+ public void getApplicationInfo_disabledApplication_stillReturnedWithNoFlags() throws Exception {
pm.setApplicationEnabledSetting(
context.getPackageName(), COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
@@ -298,13 +297,4 @@ public final class PackageManagerTest {
}
return filtered.toArray(new ActivityInfo[0]);
}
-
- private static boolean isRobolectric() {
- try {
- Class.forName("org.robolectric.RuntimeEnvironment");
- return true;
- } catch (ClassNotFoundException e) {
- return false;
- }
- }
}
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/content/res/AssetManagerTest.java b/integration_tests/ctesque/src/sharedTest/java/android/content/res/AssetManagerTest.java
index bed5a22b7..15576c69a 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/content/res/AssetManagerTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/content/res/AssetManagerTest.java
@@ -8,14 +8,13 @@ import android.os.ParcelFileDescriptor;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.io.CharStreams;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.robolectric.annotation.internal.DoNotInstrument;
@@ -26,9 +25,6 @@ import org.robolectric.annotation.internal.DoNotInstrument;
@RunWith(AndroidJUnit4.class)
public class AssetManagerTest {
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
private AssetManager assetManager;
private static final Charset UTF_8 = Charset.forName("UTF-8");
@@ -70,8 +66,10 @@ public class AssetManagerTest {
@Test
public void openFd_shouldProvideFileDescriptorForAsset() throws Exception {
AssetFileDescriptor assetFileDescriptor = assetManager.openFd("assetsHome.txt");
- assertThat(CharStreams.toString(new InputStreamReader(assetFileDescriptor.createInputStream(), UTF_8)))
- .isEqualTo("assetsHome!");
+ FileInputStream fis = assetFileDescriptor.createInputStream();
+ InputStreamReader isr = new InputStreamReader(fis, UTF_8);
+ String actual = CharStreams.toString(isr);
+ assertThat(actual).isEqualTo("assetsHome!");
assertThat(assetFileDescriptor.getLength()).isEqualTo(11);
}
@@ -84,6 +82,7 @@ public class AssetManagerTest {
+ "open_shouldProvideFileDescriptor.txt");
FileOutputStream output = new FileOutputStream(file);
output.write("hi".getBytes());
+ output.close();
ParcelFileDescriptor parcelFileDescriptor =
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
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 334356bd3..82ad1658b 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
@@ -7,6 +7,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import java.util.Calendar;
import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -16,13 +19,23 @@ import org.robolectric.annotation.internal.DoNotInstrument;
/** Tests that Robolectric's android.text.format.DateFormat support is consistent with device. */
@RunWith(AndroidJUnit4.class)
@DoNotInstrument
+@Config(qualifiers = "+en-rUS")
public class DateFormatTest {
+ private Locale originalLocale;
+ private TimeZone originalTimeZone;
private Date dateAM;
private Date datePM;
@Before
public void setDate() {
+ // Always set default Locale+Timezone in any time-related unit test to
+ // avoid flakiness when testing in non-US environments.
+ originalLocale = Locale.getDefault();
+ Locale.setDefault(Locale.US);
+ originalTimeZone = TimeZone.getDefault();
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+
Calendar c = Calendar.getInstance();
c.set(2000, 10, 25, 8, 24, 30);
dateAM = c.getTime();
@@ -30,6 +43,16 @@ public class DateFormatTest {
datePM = c.getTime();
}
+ @After
+ public void tearDown() throws Exception {
+ if (originalTimeZone != null) {
+ TimeZone.setDefault(originalTimeZone);
+ }
+ if (originalLocale != null) {
+ Locale.setDefault(originalLocale);
+ }
+ }
+
@Test
public void getLongDateFormat() {
assertThat(DateFormat.getLongDateFormat(getApplicationContext()).format(dateAM))
@@ -60,15 +83,17 @@ public class DateFormatTest {
@Test
public void getTimeFormat_am() {
- // allow both regular and thin whitespace separators
+ // Allow both ASCII and Unicode space-class separators.
+ // Output may also contain U+202F (UTF-8 E2 80 AF), a.k.a. NARROW NO-BREAK SPACE
+ // which is in the \p{Zs} Unicode category.
assertThat(DateFormat.getTimeFormat(getApplicationContext()).format(dateAM))
- .matches("8:24\\sAM");
+ .matches("8:24\\p{Z}AM");
}
@Test
public void getTimeFormat_pm() {
- // allow both regular and thin whitespace separators
+ // Allow both ASCII and Unicode space-class separators.
assertThat(DateFormat.getTimeFormat(getApplicationContext()).format(datePM))
- .matches("4:24\\sPM");
+ .matches("4:24\\p{Z}PM");
}
}
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/webkit/CookieManagerTest.java b/integration_tests/ctesque/src/sharedTest/java/android/webkit/CookieManagerTest.java
index 5e25e7e48..ea784e73b 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/webkit/CookieManagerTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/webkit/CookieManagerTest.java
@@ -50,6 +50,16 @@ public class CookieManagerTest {
}
@Test
+ public void setCookie_overrideCookieHasTheSameKey() {
+ final String httpsUrl = "https://robolectric.org/";
+ final CookieManager cookieManager = CookieManager.getInstance();
+ cookieManager.setCookie(httpsUrl, "A=100;");
+ cookieManager.setCookie(httpsUrl, "A=200;");
+ String cookie = cookieManager.getCookie(httpsUrl);
+ assertThat(cookie).isEqualTo("A=200");
+ }
+
+ @Test
public void getCookie_doesNotReturnAttributes() {
final String httpsUrl = "https://robolectric.org/";
final CookieManager cookieManager = CookieManager.getInstance();
diff --git a/integration_tests/jacoco-offline/build.gradle b/integration_tests/jacoco-offline/build.gradle
new file mode 100644
index 000000000..e5d3bb5eb
--- /dev/null
+++ b/integration_tests/jacoco-offline/build.gradle
@@ -0,0 +1,92 @@
+import org.robolectric.gradle.RoboJavaModulePlugin
+
+apply plugin: RoboJavaModulePlugin
+apply plugin: "jacoco"
+
+jacoco {
+ toolVersion = jacocoVersion
+}
+
+configurations {
+ jacocoAnt
+ jacocoRuntime
+}
+
+
+dependencies {
+ testCompileOnly AndroidSdk.MAX_SDK.coordinates
+ testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
+
+ testImplementation project(":robolectric")
+ testImplementation "junit:junit:$junitVersion"
+ testImplementation "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime"
+}
+
+def unitTestTaskName = "test"
+
+def compileSourceTaskName = "classes"
+
+def javaDirPath = "${buildDir.path}/classes/java/main"
+
+def kotlinDirPath = "${buildDir.path}/classes/kotlin/main"
+
+def jacocoInstrumentedClassesOutputDirPath = "${buildDir.path}/classes/java/classes-instrumented"
+
+// make sure it's evaluated after the AGP evaluation.
+afterEvaluate {
+ tasks[compileSourceTaskName].doLast {
+ println "[JaCoCo]:Generating JaCoCo instrumented classes for the build."
+
+ def jacocoInstrumentOutputDirPathFile = new File(jacocoInstrumentedClassesOutputDirPath)
+ if (jacocoInstrumentOutputDirPathFile.exists()) {
+ println "[JaCoCo]:Classes had been instrumented."
+ return
+ }
+
+ ant.taskdef(name: 'instrument',
+ classname: 'org.jacoco.ant.InstrumentTask',
+ classpath: configurations.jacocoAnt.asPath)
+
+ def classesDirPathFile = new File(javaDirPath)
+ if (classesDirPathFile.exists()) {
+ ant.instrument(destdir: jacocoInstrumentedClassesOutputDirPath) {
+ fileset(
+ dir: javaDirPath,
+ excludes: []
+ )
+ }
+ } else {
+ println "Classes directory with path: " + classesDirPathFile + " was not existed."
+ }
+
+ def classesDirPathFileKotlin = new File(kotlinDirPath)
+ if (classesDirPathFileKotlin.exists()) {
+ ant.instrument(destdir: jacocoInstrumentedClassesOutputDirPath) {
+ fileset(
+ dir: kotlinDirPath,
+ excludes: []
+ )
+ }
+ } else {
+ println "Classes directory with path: " + classesDirPathFileKotlin + " was not existed."
+ }
+ }
+
+ def executionDataFilePath = "${buildDir.path}/jacoco/${unitTestTaskName}.exec"
+
+ // put JaCoCo instrumented classes and JaCoCoRuntime to the beginning of the JVM classpath.
+ tasks["${unitTestTaskName}"].doFirst {
+ jacoco {
+ // disable JaCoCo on-the-fly from Gradle JaCoCo plugin.
+ enabled = false
+ }
+
+ println "[JaCoCo]:Modifying classpath of tests JVM."
+
+ systemProperty 'jacoco-agent.destfile', executionDataFilePath
+
+ classpath = files(jacocoInstrumentedClassesOutputDirPath) + classpath + configurations.jacocoRuntime
+
+ println "Final test JVM classpath is ${classpath.getAsPath()}"
+ }
+}
diff --git a/integration_tests/jacoco-offline/src/main/java/org/robolectric/integrationtests/jacoco/JaCoCoTester.java b/integration_tests/jacoco-offline/src/main/java/org/robolectric/integrationtests/jacoco/JaCoCoTester.java
new file mode 100644
index 000000000..6a37f102c
--- /dev/null
+++ b/integration_tests/jacoco-offline/src/main/java/org/robolectric/integrationtests/jacoco/JaCoCoTester.java
@@ -0,0 +1,10 @@
+package org.robolectric.integrationtests.jacoco;
+
+/** A class that gets instrumented by both Robolectric (for shadowing) and Jacoco. */
+public class JaCoCoTester {
+ public static final int VALUE = 1;
+
+ public int getValue() {
+ return VALUE;
+ }
+}
diff --git a/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/JaCoCoTesterTest.java b/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/JaCoCoTesterTest.java
new file mode 100644
index 000000000..ccb073bdd
--- /dev/null
+++ b/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/JaCoCoTesterTest.java
@@ -0,0 +1,15 @@
+package org.robolectric.integrationtests.jacoco;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link JaCoCoTester}. */
+@RunWith(RobolectricTestRunner.class)
+public class JaCoCoTesterTest {
+ @Test
+ public void testGetValueBeforeShadow() {
+ Assert.assertEquals(JaCoCoTester.VALUE, new JaCoCoTester().getValue());
+ }
+}
diff --git a/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/ShadowJaCoCoTester.java b/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/ShadowJaCoCoTester.java
new file mode 100644
index 000000000..fa729f799
--- /dev/null
+++ b/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/ShadowJaCoCoTester.java
@@ -0,0 +1,15 @@
+package org.robolectric.integrationtests.jacoco;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/** Shadow for {@link JaCoCoTester}. */
+@Implements(JaCoCoTester.class)
+public class ShadowJaCoCoTester {
+ public static final int VALUE = 0;
+
+ @Implementation
+ public int getValue() {
+ return VALUE;
+ }
+}
diff --git a/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/ShadowJaCoCoTesterTest.java b/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/ShadowJaCoCoTesterTest.java
new file mode 100644
index 000000000..29b079e03
--- /dev/null
+++ b/integration_tests/jacoco-offline/src/test/java/org/robolectric/integrationtests/jacoco/ShadowJaCoCoTesterTest.java
@@ -0,0 +1,21 @@
+package org.robolectric.integrationtests.jacoco;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link ShadowJaCoCoTester}. */
+@Config(
+ shadows = {
+ ShadowJaCoCoTester.class,
+ })
+@RunWith(RobolectricTestRunner.class)
+public class ShadowJaCoCoTesterTest {
+ @Test
+ public void testGetValue() {
+ Assert.assertNotEquals(JaCoCoTester.VALUE, ShadowJaCoCoTester.VALUE);
+ Assert.assertEquals(ShadowJaCoCoTester.VALUE, new JaCoCoTester().getValue());
+ }
+}
diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle
new file mode 100644
index 000000000..68c5c677f
--- /dev/null
+++ b/integration_tests/kotlin/build.gradle
@@ -0,0 +1,28 @@
+import org.robolectric.gradle.RoboJavaModulePlugin
+
+apply plugin: RoboJavaModulePlugin
+apply plugin: 'kotlin'
+apply plugin: "com.diffplug.spotless"
+
+spotless {
+ kotlin {
+ target '**/*.kt'
+ ktfmt('0.42').googleStyle()
+ }
+}
+
+compileKotlin {
+ compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+}
+
+dependencies {
+ api project(":robolectric")
+ compileOnly AndroidSdk.MAX_SDK.coordinates
+
+ testCompileOnly AndroidSdk.MAX_SDK.coordinates
+ testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
+ testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+ testImplementation "junit:junit:$junitVersion"
+ testImplementation "com.google.truth:truth:$truthVersion"
+ testImplementation "androidx.test:core:$axtCoreVersion@aar"
+}
diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/CustomShadowImageView.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/CustomShadowImageView.kt
new file mode 100644
index 000000000..9b2a83a4c
--- /dev/null
+++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/CustomShadowImageView.kt
@@ -0,0 +1,21 @@
+package org.robolectric.integrationtests.kotlin
+
+import android.widget.ImageView
+import androidx.annotation.DrawableRes
+import org.robolectric.annotation.Implementation
+import org.robolectric.annotation.Implements
+import org.robolectric.annotation.RealObject
+
+@Implements(ImageView::class)
+open class CustomShadowImageView {
+ @RealObject lateinit var realImageView: ImageView
+
+ @DrawableRes
+ var setImageResource: Int = 0
+ private set
+
+ @Implementation
+ protected fun setImageResource(resId: Int) {
+ setImageResource = resId
+ }
+}
diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/CustomShadowImageViewTest.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/CustomShadowImageViewTest.kt
new file mode 100644
index 000000000..52a5b8bde
--- /dev/null
+++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/CustomShadowImageViewTest.kt
@@ -0,0 +1,25 @@
+package org.robolectric.integrationtests.kotlin
+
+import android.widget.ImageView
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.shadow.api.Shadow
+
+@RunWith(RobolectricTestRunner::class)
+@Config(shadows = [CustomShadowImageView::class])
+class CustomShadowImageViewTest {
+ @Test
+ fun `use custom ShadowImageView`() {
+ val imageView = ImageView(ApplicationProvider.getApplicationContext())
+ val shadowImageView = Shadow.extract<CustomShadowImageView>(imageView)
+ assertThat(shadowImageView).isNotNull()
+ assertThat(shadowImageView.realImageView).isSameInstanceAs(imageView)
+ val resourceId = Int.MAX_VALUE
+ imageView.setImageResource(resourceId)
+ assertThat(shadowImageView.setImageResource).isEqualTo(resourceId)
+ }
+}
diff --git a/integration_tests/libphonenumber/build.gradle b/integration_tests/libphonenumber/build.gradle
index 84be0c776..2c27a7968 100644
--- a/integration_tests/libphonenumber/build.gradle
+++ b/integration_tests/libphonenumber/build.gradle
@@ -9,5 +9,5 @@ dependencies {
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation 'com.googlecode.libphonenumber:libphonenumber:8.0.0'
+ testImplementation 'com.googlecode.libphonenumber:libphonenumber:8.13.8'
} \ No newline at end of file
diff --git a/integration_tests/memoryleaks/build.gradle b/integration_tests/memoryleaks/build.gradle
index 743c0da5b..2cc51247c 100644
--- a/integration_tests/memoryleaks/build.gradle
+++ b/integration_tests/memoryleaks/build.gradle
@@ -30,5 +30,5 @@ dependencies {
testImplementation project(":robolectric")
testImplementation "junit:junit:$junitVersion"
testImplementation "com.google.guava:guava-testlib:$guavaJREVersion"
- testImplementation("androidx.fragment:fragment:1.3.4")
+ testImplementation "androidx.fragment:fragment:$fragmentVersion"
}
diff --git a/integration_tests/mockito-kotlin/build.gradle b/integration_tests/mockito-kotlin/build.gradle
index 14a5b0090..ae97f1b7a 100644
--- a/integration_tests/mockito-kotlin/build.gradle
+++ b/integration_tests/mockito-kotlin/build.gradle
@@ -7,7 +7,7 @@ apply plugin: "com.diffplug.spotless"
spotless {
kotlin {
target '**/*.kt'
- ktfmt('0.34').googleStyle()
+ ktfmt('0.42').googleStyle()
}
}
diff --git a/integration_tests/mockk/build.gradle b/integration_tests/mockk/build.gradle
index 2dcf2f737..78344a9aa 100644
--- a/integration_tests/mockk/build.gradle
+++ b/integration_tests/mockk/build.gradle
@@ -7,7 +7,7 @@ apply plugin: "com.diffplug.spotless"
spotless {
kotlin {
target '**/*.kt'
- ktfmt('0.34').googleStyle()
+ ktfmt('0.42').googleStyle()
}
}
@@ -23,5 +23,5 @@ dependencies {
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
testImplementation "junit:junit:${junitVersion}"
testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation 'io.mockk:mockk:1.9.3'
+ testImplementation 'io.mockk:mockk:1.13.4'
}
diff --git a/integration_tests/nativegraphics/AndroidManifest.xml b/integration_tests/nativegraphics/AndroidManifest.xml
new file mode 100644
index 000000000..cdffdf187
--- /dev/null
+++ b/integration_tests/nativegraphics/AndroidManifest.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Manifest for native graphics tests -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.robolectric.integrationtests.nativegraphics">
+ <uses-sdk
+ android:minSdkVersion="26"
+ android:targetSdkVersion="31" />
+ <application />
+</manifest>
diff --git a/integration_tests/nativegraphics/build.gradle b/integration_tests/nativegraphics/build.gradle
new file mode 100644
index 000000000..10e8f6138
--- /dev/null
+++ b/integration_tests/nativegraphics/build.gradle
@@ -0,0 +1,40 @@
+import org.robolectric.gradle.AndroidProjectConfigPlugin
+import org.robolectric.gradle.GradleManagedDevicePlugin
+
+apply plugin: 'com.android.library'
+apply plugin: AndroidProjectConfigPlugin
+apply plugin: GradleManagedDevicePlugin
+
+android {
+ compileSdk 33
+
+ defaultConfig {
+ minSdk 26
+ targetSdk 33
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ all {
+ systemProperty 'robolectric.graphicsMode', 'NATIVE'
+ }
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility 11
+ targetCompatibility 11
+ }
+}
+
+dependencies {
+ testImplementation AndroidSdk.MAX_SDK.coordinates
+ testImplementation project(':robolectric')
+
+ testImplementation "androidx.core:core:$coreVersion"
+ testImplementation "androidx.test.ext:junit:$axtJunitVersion"
+ testImplementation "com.google.truth:truth:${truthVersion}"
+ testImplementation "junit:junit:${junitVersion}"
+ testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+}
diff --git a/integration_tests/nativegraphics/src/main/AndroidManifest.xml b/integration_tests/nativegraphics/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..cdffdf187
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Manifest for native graphics tests -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.robolectric.integrationtests.nativegraphics">
+ <uses-sdk
+ android:minSdkVersion="26"
+ android:targetSdkVersion="31" />
+ <application />
+</manifest>
diff --git a/integration_tests/nativegraphics/src/main/assets/almost-red-adobe.png b/integration_tests/nativegraphics/src/main/assets/almost-red-adobe.png
new file mode 100644
index 000000000..531b5a4ea
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/almost-red-adobe.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/blue-16bit-prophoto.png b/integration_tests/nativegraphics/src/main/assets/blue-16bit-prophoto.png
new file mode 100644
index 000000000..00ccc780e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/blue-16bit-prophoto.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/blue-16bit-srgb.png b/integration_tests/nativegraphics/src/main/assets/blue-16bit-srgb.png
new file mode 100644
index 000000000..5f855f24e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/blue-16bit-srgb.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/StaticLayoutLineBreakingTestFont.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/StaticLayoutLineBreakingTestFont.ttf
new file mode 100644
index 000000000..448748350
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/StaticLayoutLineBreakingTestFont.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_g3em_weight400_upright.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_g3em_weight400_upright.ttf
new file mode 100644
index 000000000..b3175a108
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_g3em_weight400_upright.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_h3em_weight400_italic.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_h3em_weight400_italic.ttf
new file mode 100644
index 000000000..099c4f146
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_h3em_weight400_italic.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_m3em_weight700_upright.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_m3em_weight700_upright.ttf
new file mode 100644
index 000000000..062f2990f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/family_selection/ttf/ascii_m3em_weight700_upright.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/layout/linebreak.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/layout/linebreak.ttf
new file mode 100644
index 000000000..eb18c0a9b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/layout/linebreak.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/layout/samplefont.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/layout/samplefont.ttf
new file mode 100644
index 000000000..5fccad24d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/layout/samplefont.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont.ttf
new file mode 100644
index 000000000..5fccad24d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont2.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont2.ttf
new file mode 100644
index 000000000..f31b8a927
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont2.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont3.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont3.ttf
new file mode 100644
index 000000000..9c850ab28
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/others/samplefont3.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/samplefont.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/samplefont.ttf
new file mode 100644
index 000000000..218d655b2
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/samplefont.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/bombfont.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/bombfont.ttf
new file mode 100644
index 000000000..c7e50baf2
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/bombfont.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/bombfont2.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/bombfont2.ttf
new file mode 100644
index 000000000..da1207706
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/bombfont2.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/out_of_unicode_end_cmap12.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/out_of_unicode_end_cmap12.ttf
new file mode 100644
index 000000000..9d7c1210c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/out_of_unicode_end_cmap12.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/out_of_unicode_start_cmap12.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/out_of_unicode_start_cmap12.ttf
new file mode 100644
index 000000000..7d48357bd
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/out_of_unicode_start_cmap12.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/too_large_end_cmap12.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/too_large_end_cmap12.ttf
new file mode 100644
index 000000000..eb0e563c9
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/too_large_end_cmap12.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/too_large_start_cmap12.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/too_large_start_cmap12.ttf
new file mode 100644
index 000000000..1ce785aa0
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/too_large_start_cmap12.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap12.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap12.ttf
new file mode 100644
index 000000000..d9587df6f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap12.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap14_default_uvs.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap14_default_uvs.ttf
new file mode 100644
index 000000000..83801fcbb
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap14_default_uvs.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap14_non_default_uvs.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap14_non_default_uvs.ttf
new file mode 100644
index 000000000..92aadc2ed
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap14_non_default_uvs.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap4.ttf b/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap4.ttf
new file mode 100644
index 000000000..8ceeb4e8b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/fonts/security/unsorted_cmap4.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/grayscale-16bit-linearSrgb.png b/integration_tests/nativegraphics/src/main/assets/grayscale-16bit-linearSrgb.png
new file mode 100644
index 000000000..344fb1e24
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/grayscale-16bit-linearSrgb.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/grayscale-linearSrgb.png b/integration_tests/nativegraphics/src/main/assets/grayscale-linearSrgb.png
new file mode 100644
index 000000000..85f2c958e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/grayscale-linearSrgb.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/green-p3.png b/integration_tests/nativegraphics/src/main/assets/green-p3.png
new file mode 100644
index 000000000..02f4cd1c2
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/green-p3.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/green-srgb.png b/integration_tests/nativegraphics/src/main/assets/green-srgb.png
new file mode 100644
index 000000000..8b4d5efd9
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/green-srgb.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/purple-cmyk.png b/integration_tests/nativegraphics/src/main/assets/purple-cmyk.png
new file mode 100644
index 000000000..520c721dd
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/purple-cmyk.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/purple-displayprofile.png b/integration_tests/nativegraphics/src/main/assets/purple-displayprofile.png
new file mode 100644
index 000000000..644899160
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/purple-displayprofile.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/red-adobergb.png b/integration_tests/nativegraphics/src/main/assets/red-adobergb.png
new file mode 100644
index 000000000..adbff91b5
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/red-adobergb.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/assets/translucent-green-p3.png b/integration_tests/nativegraphics/src/main/assets/translucent-green-p3.png
new file mode 100644
index 000000000..8066e69fb
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/assets/translucent-green-p3.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/anim/animation_grouping_1_01.xml b/integration_tests/nativegraphics/src/main/res/anim/animation_grouping_1_01.xml
new file mode 100644
index 000000000..c1f290494
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/anim/animation_grouping_1_01.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <objectAnimator
+ android:duration="50"
+ android:propertyName="rotation"
+ android:valueFrom="0"
+ android:valueTo="180"
+ android:repeatCount="2" />
+</set> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/colors.xml b/integration_tests/nativegraphics/src/main/res/color/colors.xml
new file mode 100644
index 000000000..64f1589b7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/colors.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<resources>
+ <drawable name="red">#7f00</drawable>
+ <drawable name="blue">#770000ff</drawable>
+ <drawable name="black">#77ffffff</drawable>
+ <drawable name="yellow">#77ffff00</drawable>
+ <color name="testcolor1">#ff00ff00</color>
+ <color name="testcolor2">#ffff0000</color>
+ <color name="failColor">#ff0000ff</color>
+ <color name="colorPrimary">#008577</color>
+ <color name="colorPrimaryDark">#00574B</color>
+ <color name="colorAccent">#D81B60</color>
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear.xml
new file mode 100644
index 000000000..d9b74971f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:startColor="?attr/themeColor"
+ android:endColor="#0f0"
+ android:centerColor="#00ff0000"
+ android:startX="0"
+ android:startY="0"
+ android:endX="100"
+ android:endY="100"
+ android:type="linear">
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_clamp.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_clamp.xml
new file mode 100644
index 000000000..56d9fc889
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_clamp.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:startColor="?attr/themeColor"
+ android:endColor="#0f0"
+ android:centerColor="#00ff0000"
+ android:startX="0"
+ android:startY="0"
+ android:endX="50"
+ android:endY="50"
+ android:type="linear"
+ android:tileMode="clamp">
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item.xml
new file mode 100644
index 000000000..c1fb56060
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:startColor="?attr/themeColor"
+ android:endColor="#0f0"
+ android:centerColor="#f00"
+ android:startX="0"
+ android:startY="0"
+ android:endX="100"
+ android:endY="100"
+ android:type="linear">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#fff"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_overlap.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_overlap.xml
new file mode 100644
index 000000000..a5b261a5c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_overlap.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:startColor="?attr/themeColor"
+ android:endColor="#0f0"
+ android:centerColor="#f00"
+ android:startX="0"
+ android:startY="0"
+ android:endX="100"
+ android:endY="100"
+ android:type="linear">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#f00"/>
+ <item android:offset="0.4" android:color="#fff"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_overlap_mirror.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_overlap_mirror.xml
new file mode 100644
index 000000000..009eb526a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_overlap_mirror.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:startColor="?attr/themeColor"
+ android:endColor="#0f0"
+ android:centerColor="#f00"
+ android:startX="0"
+ android:startY="0"
+ android:endX="50"
+ android:endY="50"
+ android:type="linear"
+ android:tileMode="mirror">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#f00"/>
+ <item android:offset="0.4" android:color="#fff"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_repeat.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_repeat.xml
new file mode 100644
index 000000000..c89e98156
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_linear_item_repeat.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:startColor="?attr/themeColor"
+ android:endColor="#0f0"
+ android:centerColor="#f00"
+ android:startX="0"
+ android:startY="0"
+ android:endX="50"
+ android:endY="50"
+ android:type="linear"
+ android:tileMode="repeat">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#fff"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial.xml
new file mode 100644
index 000000000..389a0fc83
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerColor="#ff0000"
+ android:endColor="#0f0"
+ android:centerX="300"
+ android:centerY="300"
+ android:gradientRadius="100"
+ android:startColor="?attr/themeColor"
+ android:type="radial">
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_clamp.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_clamp.xml
new file mode 100644
index 000000000..ff291345b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_clamp.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerColor="#ff0000"
+ android:endColor="#0f0"
+ android:centerX="300"
+ android:centerY="300"
+ android:gradientRadius="50"
+ android:startColor="?attr/themeColor"
+ android:type="radial"
+ android:tileMode="clamp">
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item.xml
new file mode 100644
index 000000000..2f116c901
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerColor="#ff0000"
+ android:endColor="#ff0000ff"
+ android:centerX="300"
+ android:centerY="300"
+ android:gradientRadius="100"
+ android:startColor="#ffffffff"
+ android:type="radial">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#fff"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_repeat.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_repeat.xml
new file mode 100644
index 000000000..d17fc4866
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_repeat.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerColor="#ff0000"
+ android:endColor="#ff0000ff"
+ android:centerX="300"
+ android:centerY="300"
+ android:gradientRadius="50"
+ android:startColor="#ffffffff"
+ android:type="radial"
+ android:tileMode="repeat">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#fff"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_short.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_short.xml
new file mode 100644
index 000000000..111d02342
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_short.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerX="300"
+ android:centerY="300"
+ android:gradientRadius="100"
+ android:type="radial">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_short_mirror.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_short_mirror.xml
new file mode 100644
index 000000000..1aa110c33
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_radial_item_short_mirror.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerX="300"
+ android:centerY="300"
+ android:gradientRadius="50"
+ android:type="radial"
+ android:tileMode="mirror">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep.xml
new file mode 100644
index 000000000..e1fbd10b7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerColor="#ff0000"
+ android:endColor="#ff0000ff"
+ android:centerX="500"
+ android:centerY="500"
+ android:gradientRadius="10"
+ android:startColor="#ffffffff"
+ android:type="sweep">
+</gradient>
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_clamp.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_clamp.xml
new file mode 100644
index 000000000..80f39f3ee
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_clamp.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerColor="#ff0000"
+ android:endColor="#ff0000ff"
+ android:centerX="500"
+ android:centerY="500"
+ android:gradientRadius="10"
+ android:startColor="#ffffffff"
+ android:type="sweep"
+ android:tileMode="clamp">
+</gradient>
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item.xml
new file mode 100644
index 000000000..2a010c07f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerColor="#ff0000"
+ android:endColor="#ff0000ff"
+ android:centerX="500"
+ android:centerY="500"
+ android:gradientRadius="10"
+ android:startColor="#ffffffff"
+ android:type="sweep">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#fff"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient>
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_long.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_long.xml
new file mode 100644
index 000000000..7b0bbef89
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_long.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerX="500"
+ android:centerY="500"
+ android:gradientRadius="10"
+ android:type="sweep">
+ <item android:offset="-0.3" android:color="#f00"/>
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#0f0"/>
+ <item android:offset="0.6" android:color="#00f"/>
+ <item android:offset="0.7" android:color="#0f0"/>
+ <item android:offset="1.5" android:color="#00f"/>
+</gradient>
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_long_mirror.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_long_mirror.xml
new file mode 100644
index 000000000..2a73a4ffd
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_long_mirror.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerX="500"
+ android:centerY="500"
+ android:gradientRadius="10"
+ android:type="sweep"
+ android:tileMode="mirror">
+ <item android:offset="-0.3" android:color="#f00"/>
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#0f0"/>
+ <item android:offset="0.6" android:color="#00f"/>
+ <item android:offset="0.7" android:color="#0f0"/>
+ <item android:offset="1.5" android:color="#00f"/>
+</gradient>
diff --git a/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_repeat.xml b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_repeat.xml
new file mode 100644
index 000000000..62e6f66b4
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/fill_gradient_sweep_item_repeat.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:centerColor="#ff0000"
+ android:endColor="#ff0000ff"
+ android:centerX="500"
+ android:centerY="500"
+ android:gradientRadius="10"
+ android:startColor="#ffffffff"
+ android:type="sweep"
+ android:tileMode="repeat">
+ <item android:offset="0.1" android:color="?attr/themeColor"/>
+ <item android:offset="0.4" android:color="#fff"/>
+ <item android:offset="0.9" android:color="#0f0"/>
+</gradient>
diff --git a/integration_tests/nativegraphics/src/main/res/color/stroke_gradient.xml b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient.xml
new file mode 100644
index 000000000..cb324c9a7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:centerColor="#7f7f7f"
+ android:endColor="#ffffff"
+ android:startColor="#000000"
+ android:startX="0"
+ android:endX="100"
+ android:startY="0"
+ android:endY="0"
+ android:type="linear">
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_clamp.xml b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_clamp.xml
new file mode 100644
index 000000000..3d746e720
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_clamp.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:centerColor="#7f7f7f"
+ android:endColor="#ffffff"
+ android:startColor="#000000"
+ android:startX="0"
+ android:endX="50"
+ android:startY="0"
+ android:endY="0"
+ android:type="linear"
+ android:tileMode="clamp">
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item.xml b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item.xml
new file mode 100644
index 000000000..15d948c25
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:centerColor="#7f7f7f"
+ android:endColor="#ffffff"
+ android:startColor="#000000"
+ android:startX="0"
+ android:endX="100"
+ android:startY="0"
+ android:endY="0"
+ android:type="linear">
+ <item android:offset="0.1" android:color="#f00"/>
+ <item android:offset="0.2" android:color="#f0f"/>
+ <item android:offset="0.9" android:color="#f00f"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_alpha.xml b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_alpha.xml
new file mode 100644
index 000000000..fda2b88bc
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_alpha.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:startX="0"
+ android:endX="100"
+ android:startY="0"
+ android:endY="0"
+ android:type="linear">
+ <item android:offset="0.1" android:color="#f00"/>
+ <item android:offset="0.2" android:color="#2f0f"/>
+ <item android:offset="0.9" android:color="#f00f"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_alpha_mirror.xml b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_alpha_mirror.xml
new file mode 100644
index 000000000..352a2fd46
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_alpha_mirror.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:startX="0"
+ android:endX="50"
+ android:startY="0"
+ android:endY="0"
+ android:type="linear"
+ android:tileMode="mirror">
+ <item android:offset="0.1" android:color="#f00"/>
+ <item android:offset="0.2" android:color="#2f0f"/>
+ <item android:offset="0.9" android:color="#f00f"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_repeat.xml b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_repeat.xml
new file mode 100644
index 000000000..42281d15d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/stroke_gradient_item_repeat.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<gradient xmlns:android="http://schemas.android.com/apk/res/android"
+ android:angle="90"
+ android:centerColor="#7f7f7f"
+ android:endColor="#ffffff"
+ android:startColor="#000000"
+ android:startX="0"
+ android:endX="50"
+ android:startY="0"
+ android:endY="0"
+ android:type="linear"
+ android:tileMode="repeat">
+ <item android:offset="0.1" android:color="#f00"/>
+ <item android:offset="0.2" android:color="#f0f"/>
+ <item android:offset="0.9" android:color="#f00f"/>
+</gradient> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/styles.xml b/integration_tests/nativegraphics/src/main/res/color/styles.xml
new file mode 100644
index 000000000..7e05f0ab5
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/styles.xml
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="Whatever">
+ <item name="type1">true</item>
+ <item name="type2">false</item>
+ <item name="type3">#ff0000ff</item>
+ <item name="type4">#ff00ff00</item>
+ <item name="type5">0.75px</item>
+ <item name="type6">10px</item>
+ <item name="type7">18px</item>
+ <item name="type8">@drawable/pass</item>
+ <item name="type9">3.14</item>
+ <item name="type10">100%</item>
+ <item name="type11">365</item>
+ <item name="type12">86400</item>
+ <item name="type13">@string/hello_android</item>
+ <item name="type14">TypedArray Test!</item>
+ <item name="type15">@array/difficultyLevel</item>
+ <item name="type16">Typed Value!</item>
+ </style>
+
+ <style name="TextViewWithoutColorAndAppearance">
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <style name="TextViewWithColorButWithOutAppearance">
+ <item name="android:textColor">#ff0000ff</item>
+ </style>
+
+ <style name="TextViewWithColorAndAppearance">
+ <item name="android:textColor">#ff0000ff</item>
+ <item name="android:textAppearance">@style/TextAppearance.WithColor</item>
+ </style>
+
+ <style name="TextViewWithoutColorButWithAppearance">
+ <item name="android:textAppearance">@style/TextAppearance.WithColor</item>
+ </style>
+
+ <style name="TextAppearance" parent="android:TextAppearance">
+ </style>
+
+ <style name="TextAppearance.WithColor">
+ <item name="android:textColor">#ffff0000</item>
+ </style>
+
+ <style name="TextAppearance.All">
+ <item name="android:textColor">@drawable/black</item>
+ <item name="android:textSize">20px</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textColorHint">@drawable/red</item>
+ <item name="android:textColorLink">@drawable/blue</item>
+ <item name="android:textColorHighlight">@drawable/yellow</item>
+ </style>
+
+ <style name="TextAppearance.Colors">
+ <item name="android:textColor">@drawable/black</item>
+ <item name="android:textColorHint">@drawable/blue</item>
+ <item name="android:textColorLink">@drawable/yellow</item>
+ <item name="android:textColorHighlight">@drawable/red</item>
+ </style>
+
+ <style name="TextAppearance.NotColors">
+ <item name="android:textSize">17px</item>
+ <item name="android:typeface">sans</item>
+ <item name="android:textStyle">normal</item>
+ </style>
+
+ <style name="TextAppearance.Style">
+ <item name="android:textStyle">normal</item>
+ </style>
+
+ <style name="TestEnum1">
+ <item name="testEnum">val1</item>
+ </style>
+
+ <style name="TestEnum2">
+ <item name="testEnum">val2</item>
+ </style>
+
+ <style name="TestEnum10">
+ <item name="testEnum">val10</item>
+ </style>
+
+ <style name="TestFlag1">
+ <item name="testFlags">bit1</item>
+ </style>
+
+ <style name="TestFlag2">
+ <item name="testFlags">bit2</item>
+ </style>
+
+ <style name="TestFlag31">
+ <item name="testFlags">bit31</item>
+ </style>
+
+ <style name="TestFlag1And2">
+ <item name="testFlags">bit1|bit2</item>
+ </style>
+
+ <style name="TestFlag1And2And31">
+ <item name="testFlags">bit1|bit2|bit31</item>
+ </style>
+
+ <style name="TestEnum1.EmptyInherit" />
+
+ <style name="Theme_AlertDialog">
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <style name="TestProgressBar">
+ <item name="android:indeterminateOnly">false</item>
+ <item name="android:progressDrawable">@android:drawable/progress_horizontal</item>
+ <item name="android:indeterminateDrawable">@android:drawable/progress_horizontal</item>
+ <item name="android:minHeight">20dip</item>
+ <item name="android:maxHeight">20dip</item>
+ <item name="android:focusable">true</item>
+ </style>
+
+ <style name="Test_Theme">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:panelColorForeground">#ff000000</item>
+ <item name="android:panelColorBackground">#ffffffff</item>
+ </style>
+
+ <style name="Theme_OverrideOuter">
+ <item name="themeType">1</item>
+ </style>
+
+ <style name="Theme_OverrideInner">
+ <item name="themeType">2</item>
+ <item name="themeOverrideAttr">@style/Theme_OverrideAttr</item>
+ </style>
+
+ <style name="Theme_OverrideAttr">
+ <item name="themeType">3</item>
+ </style>
+
+ <style name="Theme_ThemedDrawableTest">
+ <item name="themeBoolean">true</item>
+ <item name="themeColor">@android:color/black</item>
+ <item name="themeFloat">1.0</item>
+ <item name="themeAngle">45.0</item>
+ <item name="themeInteger">1</item>
+ <item name="themeDimension">1px</item>
+ <item name="themeDrawable">@drawable/icon_black</item>
+ <item name="themeBitmap">@drawable/icon_black</item>
+ <item name="themeNinePatch">@drawable/ninepatch_0</item>
+ <item name="themeGravity">48</item>
+ <item name="themeTileMode">2</item>
+ <item name="themeType">0</item>
+ <item name="themeVectorDrawableFillColor">#F00F</item>
+ </style>
+
+ <attr name="colorPrimary" format="reference|color" />
+ <attr name="colorPrimaryDark" format="reference|color" />
+ <attr name="colorAccent" format="reference|color" />
+
+ <style name="Theme_MixedGradientTheme">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+
+ <!-- overwrite the default gradient type to be radial in the theme -->
+ <item name="GradientType">1</item>
+ </style>
+
+ <!-- default gradient type of linear -->
+ <attr name="GradientType" format="integer">0</attr>
+
+ <style name="WhiteBackgroundNoWindowAnimation"
+ parent="@android:style/Theme.Holo.NoActionBar.Fullscreen">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowOverscan">true</item>
+ <item name="android:fadingEdge">none</item>
+ <item name="android:windowBackground">@android:color/white</item>
+ <item name="android:windowContentTransitions">false</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
+
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/color/vector_icon_fill_state_list.xml b/integration_tests/nativegraphics/src/main/res/color/vector_icon_fill_state_list.xml
new file mode 100644
index 000000000..f5b4632f7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/vector_icon_fill_state_list.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#00ff00" android:state_pressed="true" />
+ <item android:color="#0000ff" />
+</selector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/color/vector_icon_stroke_state_list.xml b/integration_tests/nativegraphics/src/main/res/color/vector_icon_stroke_state_list.xml
new file mode 100644
index 000000000..bbc635ea5
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/color/vector_icon_stroke_state_list.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#0000ff" android:state_pressed="true" />
+ <item android:color="#00ff00" />
+</selector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/alpha_mask.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/alpha_mask.png
new file mode 100644
index 000000000..1d6177f78
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/alpha_mask.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/black1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/black1.png
new file mode 100644
index 000000000..d1c246a3e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/black1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/blackitalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/blackitalic1.png
new file mode 100644
index 000000000..d162afc8a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/blackitalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/blue_padded_square.9.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/blue_padded_square.9.png
new file mode 100644
index 000000000..d69869db6
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/blue_padded_square.9.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/bold1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/bold1.png
new file mode 100644
index 000000000..b9f521856
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/bold1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/bolditalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/bolditalic1.png
new file mode 100644
index 000000000..3a0aa23ed
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/bolditalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensed1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensed1.png
new file mode 100644
index 000000000..88c8befb7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensed1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedbold1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedbold1.png
new file mode 100644
index 000000000..8318a5189
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedbold1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedbolditalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedbolditalic1.png
new file mode 100644
index 000000000..4cae84d25
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedbolditalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condenseditalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condenseditalic1.png
new file mode 100644
index 000000000..50d0d38d0
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condenseditalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedlight1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedlight1.png
new file mode 100644
index 000000000..df758a552
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedlight1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedlightitalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedlightitalic1.png
new file mode 100644
index 000000000..f2959d61e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/condensedlightitalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/extrabold1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/extrabold1.png
new file mode 100644
index 000000000..c247451d4
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/extrabold1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/extrabolditalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/extrabolditalic1.png
new file mode 100644
index 000000000..5900db2f8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/extrabolditalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_blue_circle.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_blue_circle.png
new file mode 100644
index 000000000..f587fb7ca
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_blue_circle.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_dashed_oval.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_dashed_oval.png
new file mode 100644
index 000000000..c95568a58
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_dashed_oval.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_scaled.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_scaled.png
new file mode 100644
index 000000000..b3acd59b6
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_scaled.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_subset.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_subset.png
new file mode 100644
index 000000000..1afc1b424
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_subset.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_transformed.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_transformed.png
new file mode 100644
index 000000000..50a19e5ae
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_create_transformed.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_ninepatch.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_ninepatch.png
new file mode 100644
index 000000000..404f6ae06
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_hardwaretest_ninepatch.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_headless_robot.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_headless_robot.png
new file mode 100644
index 000000000..0bf0272c6
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_headless_robot.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_robot.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_robot.png
new file mode 100644
index 000000000..ef3631a42
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/golden_robot.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/hello1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/hello1.png
new file mode 100644
index 000000000..6ae985031
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/hello1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/index_8.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/index_8.png
new file mode 100644
index 000000000..6d6a94f20
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/index_8.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/italic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/italic1.png
new file mode 100644
index 000000000..508318641
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/italic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/light1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/light1.png
new file mode 100644
index 000000000..bb829d71f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/light1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/lightitalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/lightitalic1.png
new file mode 100644
index 000000000..f62385459
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/lightitalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/medium1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/medium1.png
new file mode 100644
index 000000000..c4c047d86
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/medium1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/mediumitalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/mediumitalic1.png
new file mode 100644
index 000000000..1f94b8675
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/mediumitalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/padding_0.9.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/padding_0.9.png
new file mode 100644
index 000000000..0127bf4d0
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/padding_0.9.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathclippingtest_torus.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathclippingtest_torus.png
new file mode 100644
index 000000000..76dcbc554
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathclippingtest_torus.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_circle.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_circle.png
new file mode 100644
index 000000000..2084f2389
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_circle.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_cubics.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_cubics.png
new file mode 100644
index 000000000..96079c6f8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_cubics.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_quads.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_quads.png
new file mode 100644
index 000000000..533235460
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_quads.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_rect.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_rect.png
new file mode 100644
index 000000000..0a7bab352
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/pathtest_path_approximate_rect.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/robot.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/robot.png
new file mode 100644
index 000000000..72a065cb8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/robot.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/text_on_path.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/text_on_path.png
new file mode 100644
index 000000000..d0dee6cac
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/text_on_path.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/text_path_with_offset.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/text_path_with_offset.png
new file mode 100644
index 000000000..533d1eace
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/text_path_with_offset.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/thin1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/thin1.png
new file mode 100644
index 000000000..af0aabd11
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/thin1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/thinitalic1.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/thinitalic1.png
new file mode 100644
index 000000000..da6c3bc31
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/thinitalic1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_drawable_scale_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_drawable_scale_golden.png
new file mode 100644
index 000000000..748e56144
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_drawable_scale_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_arcto_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_arcto_golden.png
new file mode 100644
index 000000000..d1ef33bf3
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_arcto_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_clip_path_1_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_clip_path_1_golden.png
new file mode 100644
index 000000000..2663f3c8a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_clip_path_1_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_create_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_create_golden.png
new file mode 100644
index 000000000..535b4356b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_create_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_delete_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_delete_golden.png
new file mode 100644
index 000000000..75805c617
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_delete_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_filltype_evenodd_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_filltype_evenodd_golden.png
new file mode 100644
index 000000000..5765d1d43
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_filltype_evenodd_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_filltype_nonzero_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_filltype_nonzero_golden.png
new file mode 100644
index 000000000..587ca1e04
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_filltype_nonzero_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_1_clamp_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_1_clamp_golden.png
new file mode 100644
index 000000000..e67fce8d5
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_1_clamp_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_1_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_1_golden.png
new file mode 100644
index 000000000..8a4810411
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_1_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_2_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_2_golden.png
new file mode 100644
index 000000000..2145eec80
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_2_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_2_repeat_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_2_repeat_golden.png
new file mode 100644
index 000000000..542805273
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_2_repeat_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_3_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_3_golden.png
new file mode 100644
index 000000000..70ee76afe
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_3_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_3_mirror_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_3_mirror_golden.png
new file mode 100644
index 000000000..0a195a846
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_gradient_3_mirror_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_group_clip_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_group_clip_golden.png
new file mode 100644
index 000000000..f22746500
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_group_clip_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_heart_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_heart_golden.png
new file mode 100644
index 000000000..051689ab4
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_heart_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_implicit_lineto_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_implicit_lineto_golden.png
new file mode 100644
index 000000000..b18a6bfb6
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_implicit_lineto_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_random_path_1_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_random_path_1_golden.png
new file mode 100644
index 000000000..90442f016
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_random_path_1_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_random_path_2_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_random_path_2_golden.png
new file mode 100644
index 000000000..c9d8e1f0f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_random_path_2_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_render_order_1_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_render_order_1_golden.png
new file mode 100644
index 000000000..7b2f066d8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_render_order_1_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_render_order_2_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_render_order_2_golden.png
new file mode 100644
index 000000000..96b2fc949
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_render_order_2_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_a_1_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_a_1_golden.png
new file mode 100644
index 000000000..b78975c8e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_a_1_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_a_2_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_a_2_golden.png
new file mode 100644
index 000000000..26d68462f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_a_2_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_cq_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_cq_golden.png
new file mode 100644
index 000000000..74f5ed7d0
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_cq_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_st_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_st_golden.png
new file mode 100644
index 000000000..d08870abc
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_repeated_st_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_1_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_1_golden.png
new file mode 100644
index 000000000..4b02211f1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_1_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_2_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_2_golden.png
new file mode 100644
index 000000000..ac67731b7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_2_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_3_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_3_golden.png
new file mode 100644
index 000000000..49639cb7e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_scale_3_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_schedule_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_schedule_golden.png
new file mode 100644
index 000000000..b5163e906
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_schedule_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_settings_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_settings_golden.png
new file mode 100644
index 000000000..39f26aa41
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_settings_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_2_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_2_golden.png
new file mode 100644
index 000000000..e0902730a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_2_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_2_pressed_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_2_pressed_golden.png
new file mode 100644
index 000000000..fddd4ecdd
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_2_pressed_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_golden.png
new file mode 100644
index 000000000..e0902730a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_pressed_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_pressed_golden.png
new file mode 100644
index 000000000..fddd4ecdd
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_state_list_pressed_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_1_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_1_golden.png
new file mode 100644
index 000000000..f016ee539
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_1_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_2_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_2_golden.png
new file mode 100644
index 000000000..c09c1d9fd
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_2_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_3_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_3_golden.png
new file mode 100644
index 000000000..d9884a68d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_stroke_3_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_1_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_1_golden.png
new file mode 100644
index 000000000..f81983907
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_1_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_2_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_2_golden.png
new file mode 100644
index 000000000..03b82e35c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_2_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_3_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_3_golden.png
new file mode 100644
index 000000000..352798bae
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_3_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_4_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_4_golden.png
new file mode 100644
index 000000000..d91fa66db
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_4_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_5_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_5_golden.png
new file mode 100644
index 000000000..530bb51a9
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_5_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_6_golden.png b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_6_golden.png
new file mode 100644
index 000000000..ec2ad7ee4
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable-nodpi/vector_icon_transformation_6_golden.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/alpha.png b/integration_tests/nativegraphics/src/main/res/drawable/alpha.png
new file mode 100644
index 000000000..8a8854873
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/alpha.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/animated.gif b/integration_tests/nativegraphics/src/main/res/drawable/animated.gif
new file mode 100644
index 000000000..51baf1586
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/animated.gif
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/animation_vector_drawable_grouping_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/animation_vector_drawable_grouping_1.xml
new file mode 100644
index 000000000..4a7e4f6d8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/animation_vector_drawable_grouping_1.xml
@@ -0,0 +1,26 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/vector_drawable_grouping_1" >
+
+ <target
+ android:name="sun"
+ android:animation="@anim/animation_grouping_1_01" />
+ <target
+ android:name="earth"
+ android:animation="@anim/animation_grouping_1_01" />
+
+</animated-vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/baseline_jpeg.jpg b/integration_tests/nativegraphics/src/main/res/drawable/baseline_jpeg.jpg
new file mode 100644
index 000000000..ed5251c84
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/baseline_jpeg.jpg
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/bitmap_density.xml b/integration_tests/nativegraphics/src/main/res/drawable/bitmap_density.xml
new file mode 100644
index 000000000..04d61254d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/bitmap_density.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/icon_blue" />
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/bitmap_shader_am_density.xml b/integration_tests/nativegraphics/src/main/res/drawable/bitmap_shader_am_density.xml
new file mode 100644
index 000000000..9c28199dc
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/bitmap_shader_am_density.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/icon_blue"
+ android:tileModeX="repeat"
+ android:tileModeY="clamp"
+ android:autoMirrored="true" />
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/bitmap_shader_density.xml b/integration_tests/nativegraphics/src/main/res/drawable/bitmap_shader_density.xml
new file mode 100644
index 000000000..216874e0d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/bitmap_shader_density.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/icon_blue"
+ android:tileModeX="repeat"
+ android:tileModeY="clamp" />
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/bmp_test.bmp b/integration_tests/nativegraphics/src/main/res/drawable/bmp_test.bmp
new file mode 100644
index 000000000..5ec6dd4d9
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/bmp_test.bmp
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/circle.xml b/integration_tests/nativegraphics/src/main/res/drawable/circle.xml
new file mode 100644
index 000000000..9d47a8a79
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/circle.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="90px"
+ android:width="90px"
+ android:viewportHeight="1"
+ android:viewportWidth="1" >
+
+ <group>
+ <path
+ android:name="box0"
+ android:pathData="m0,0.5a0.5,0.5 0 1,0 1,0a0.5,0.5 0 1,0 -1,0"
+ android:fillColor="#FF0000" />
+ </group>
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/dashed_oval.xml b/integration_tests/nativegraphics/src/main/res/drawable/dashed_oval.xml
new file mode 100644
index 000000000..904b016d6
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/dashed_oval.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<shape
+ android:shape="oval"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <stroke
+ android:width="2px"
+ android:dashGap="6px"
+ android:dashWidth="2px"
+ android:color="@android:color/black"
+ />
+</shape>
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/gif_test.gif b/integration_tests/nativegraphics/src/main/res/drawable/gif_test.gif
new file mode 100644
index 000000000..d1c2815db
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/gif_test.gif
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/google_logo_1.png b/integration_tests/nativegraphics/src/main/res/drawable/google_logo_1.png
new file mode 100644
index 000000000..6e038fc81
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/google_logo_1.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/google_logo_2.webp b/integration_tests/nativegraphics/src/main/res/drawable/google_logo_2.webp
new file mode 100644
index 000000000..f92c42bfb
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/google_logo_2.webp
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/grayscale_jpg.jpg b/integration_tests/nativegraphics/src/main/res/drawable/grayscale_jpg.jpg
new file mode 100644
index 000000000..6c6ae32c9
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/grayscale_jpg.jpg
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/icon_black.jpg b/integration_tests/nativegraphics/src/main/res/drawable/icon_black.jpg
new file mode 100644
index 000000000..4c9062a6d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/icon_black.jpg
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/icon_blue.jpg b/integration_tests/nativegraphics/src/main/res/drawable/icon_blue.jpg
new file mode 100644
index 000000000..9e6c1c86b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/icon_blue.jpg
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/icon_blue_png.png b/integration_tests/nativegraphics/src/main/res/drawable/icon_blue_png.png
new file mode 100644
index 000000000..03105fe4e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/icon_blue_png.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/ninepatch_0.9.png b/integration_tests/nativegraphics/src/main/res/drawable/ninepatch_0.9.png
new file mode 100644
index 000000000..13a12cbab
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/ninepatch_0.9.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/ninepatchdrawable.xml b/integration_tests/nativegraphics/src/main/res/drawable/ninepatchdrawable.xml
new file mode 100644
index 000000000..6e72457ad
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/ninepatchdrawable.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<nine-patch xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ninepatch_0"
+ android:dither="true"
+/>
+
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/pass.jpg b/integration_tests/nativegraphics/src/main/res/drawable/pass.jpg
new file mode 100644
index 000000000..2f4b083c1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/pass.jpg
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/png_test.png b/integration_tests/nativegraphics/src/main/res/drawable/png_test.png
new file mode 100644
index 000000000..523005168
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/png_test.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/premul_data.png b/integration_tests/nativegraphics/src/main/res/drawable/premul_data.png
new file mode 100644
index 000000000..92d7e376d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/premul_data.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/rectangle.xml b/integration_tests/nativegraphics/src/main/res/drawable/rectangle.xml
new file mode 100644
index 000000000..43c539fb8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/rectangle.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="90px"
+ android:width="90px"
+ android:viewportHeight="1"
+ android:viewportWidth="1" >
+
+ <group>
+ <path
+ android:name="box0"
+ android:pathData="m0,0l1,0l0,1l-1,0l0-1z"
+ android:fillColor="#FF0000" />
+ </group>
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/robot.png b/integration_tests/nativegraphics/src/main/res/drawable/robot.png
new file mode 100644
index 000000000..8a9e6984b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/robot.png
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/start.jpg b/integration_tests/nativegraphics/src/main/res/drawable/start.jpg
new file mode 100644
index 000000000..54e05e0d4
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/start.jpg
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/sunset1.jpg b/integration_tests/nativegraphics/src/main/res/drawable/sunset1.jpg
new file mode 100644
index 000000000..3b30b364d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/sunset1.jpg
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_density.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_density.xml
new file mode 100644
index 000000000..c3ca19819
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_density.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24" >
+
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M16.0,5.0c-1.955.0 -3.83,1.268 -4.5,3.0c-0.67-1.732 -2.547-3.0 -4.5-3.0C4.4570007,5.0 2.5,6.931999 2.5,9.5c0.0,3.529 3.793,6.258 9.0,11.5c5.207-5.242 9.0-7.971 9.0-11.5C20.5,6.931999 18.543,5.0 16.0,5.0z" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_drawable_grouping_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_drawable_grouping_1.xml
new file mode 100644
index 000000000..1010ce3e2
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_drawable_grouping_1.xml
@@ -0,0 +1,56 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:opticalInsetLeft="10px"
+ android:opticalInsetTop="20px"
+ android:opticalInsetRight="30px"
+ android:opticalInsetBottom="40px"
+ android:viewportHeight="256"
+ android:viewportWidth="256" >
+
+ <group
+ android:name="shape_layer_1"
+ android:translateX="128"
+ android:translateY="128" >
+ <group android:name="sun" >
+ <path
+ android:name="ellipse_path_1"
+ android:fillColor="#ffff8000"
+ android:pathData="m -25 0 a 25,25 0 1,0 50,0 a 25,25 0 1,0 -50,0" />
+
+ <group
+ android:name="earth"
+ android:translateX="75" >
+ <path
+ android:name="ellipse_path_1_1"
+ android:fillColor="#ff5656ea"
+ android:pathData="m -10 0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0" />
+
+ <group
+ android:name="moon"
+ android:translateX="25" >
+ <path
+ android:name="ellipse_path_1_2"
+ android:fillColor="#ffadadad"
+ android:pathData="m -5 0 a 5,5 0 1,0 10,0 a 5,5 0 1,0 -10,0" />
+ </group>
+ </group>
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_arcto.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_arcto.xml
new file mode 100644
index 000000000..6038d2b0b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_arcto.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="50dp"
+ android:viewportWidth="20.0"
+ android:viewportHeight="50.0">
+ <path
+ android:pathData="M14.285706,47.362198A50.71429,62.14286 0,0 0,1.0630035 5.5146027"
+ android:fillColor="#ff55ff"/>
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_clip_path_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_clip_path_1.xml
new file mode 100644
index 000000000..53a9660a2
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_clip_path_1.xml
@@ -0,0 +1,79 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:viewportHeight="12.25"
+ android:viewportWidth="7.30625"
+ android:width="64dp" >
+
+ <group
+ android:pivotX="3.65"
+ android:pivotY="6.125"
+ android:rotation="-30" >
+ <clip-path
+ android:name="clip1"
+ android:pathData="
+ M 0, 6.125
+ l 7.3, 0
+ l 0, 12.25
+ l -7.3, 0
+ z" />
+
+ <group
+ android:pivotX="3.65"
+ android:pivotY="6.125"
+ android:rotation="30" >
+ <path
+ android:name="one"
+ android:fillColor="#ff88ff"
+ android:pathData="M 1.215625,9.5l 1.9375,0.0 0.0-6.671875 -2.109375,0.421875 0.0-1.078125
+ l 2.09375-0.421875 1.1874998,0.0 0.0,7.75 1.9375,0.0 0.0,1.0
+ l -5.046875,0.0 0.0-1.0Z" />
+ </group>
+ </group>
+ <group
+ android:pivotX="3.65"
+ android:pivotY="6.125"
+ android:rotation="-30" >
+ <clip-path
+ android:name="clip2"
+ android:pathData="
+ M 0, 0
+ l 7.3, 0
+ l 0, 6.125
+ l -7.3, 0
+ z" />
+
+ <group
+ android:pivotX="3.65"
+ android:pivotY="6.125"
+ android:rotation="30" >
+ <path
+ android:name="two"
+ android:fillColor="#ff88ff"
+ android:pathData="M 2.534375,9.6875l 4.140625,0.0 0.0,1.0 -5.5625,0.0 0.0-1.0q 0.671875-0.6875 1.828125-1.859375
+ q 1.1718752-1.1875 1.4687502-1.53125 0.578125-0.625 0.796875-1.0625
+ q 0.234375-0.453125 0.234375-0.875 0.0-0.703125 -0.5-1.140625
+ q -0.484375-0.4375 -1.2656252-0.4375 -0.5625,0.0 -1.1875,0.1875
+ q -0.609375,0.1875 -1.3125,0.59375l 0.0-1.203125q 0.71875-0.28125 1.328125-0.421875
+ q 0.625-0.15625 1.140625-0.15625 1.3593752,0.0 2.1718752,0.6875
+ q 0.8125,0.671875 0.8125,1.8125 0.0,0.53125 -0.203125,1.015625
+ q -0.203125,0.484375 -0.734375,1.140625 -0.15625,0.171875 -0.9375,0.984375
+ q -0.78125024,0.8125 -2.2187502,2.265625Z" />
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_create.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_create.xml
new file mode 100644
index 000000000..55113f34b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_create.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:opticalInsetLeft="1px"
+ android:opticalInsetTop="2px"
+ android:opticalInsetRight="3px"
+ android:opticalInsetBottom="4px"
+ android:viewportHeight="24"
+ android:viewportWidth="24" >
+
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M3.0,17.25L3.0,21.0l3.75,0.0L17.813995,9.936001l-3.75-3.75L3.0,17.25zM20.707,7.0429993c0.391-0.391 0.391-1.023 0.0-1.414l-2.336-2.336c-0.391-0.391-1.023-0.391 -1.414,0.0l-1.832,1.832l3.75,3.75L20.707,7.0429993z" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_delete.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_delete.xml
new file mode 100644
index 000000000..7b8f2aa1c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_delete.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24" >
+
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M6.0,19.0c0.0,1.104 896e-3,2.0 2.0,2.0l8.0,0.0c1.104,0.0 2.0-896e-3 2.0-2.0l0.0-12.0L6.0,7.0L6.0,19.0zM18.0,4.0l-2.5,0.0l-1.0-1.0l-5.0,0.0l-1.0,1.0L6.0,4.0C5.4469986,4.0 5.0,4.4469986 5.0,5.0l0.0,1.0l14.0,0.0l0.0-1.0C19.0,4.4469986 18.552002,4.0 18.0,4.0z" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_filltype_evenodd.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_filltype_evenodd.xml
new file mode 100644
index 000000000..d5d86d802
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_filltype_evenodd.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector android:height="24dp" android:viewportHeight="400.0"
+ android:viewportWidth="1200.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillType="evenOdd"
+ android:fillColor="#f00"
+ android:pathData="M250,75L323,301 131,161 369,161 177,301z"
+ android:strokeColor="#000" android:strokeWidth="3"/>
+ <path android:fillType="evenOdd"
+ android:fillColor="#f00"
+ android:pathData="M600,81A107,107 0,0 1,600 295A107,107 0,0 1,600 81zM600,139A49,49 0,0 1,600 237A49,49 0,0 1,600 139z"
+ android:strokeColor="#000" android:strokeWidth="3"/>
+ <path android:fillType="evenOdd"
+ android:fillColor="#f00"
+ android:pathData="M950,81A107,107 0,0 1,950 295A107,107 0,0 1,950 81zM950,139A49,49 0,0 0,950 237A49,49 0,0 0,950 139z"
+ android:strokeColor="#000" android:strokeWidth="3"/>
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_filltype_nonzero.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_filltype_nonzero.xml
new file mode 100644
index 000000000..9754e4bed
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_filltype_nonzero.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector android:height="24dp" android:viewportHeight="400.0"
+ android:viewportWidth="1200.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillType="nonZero"
+ android:fillColor="#f00"
+ android:pathData="M250,75L323,301 131,161 369,161 177,301z"
+ android:strokeColor="#000" android:strokeWidth="3"/>
+ <path android:fillType="nonZero"
+ android:fillColor="#f00"
+ android:pathData="M600,81A107,107 0,0 1,600 295A107,107 0,0 1,600 81zM600,139A49,49 0,0 1,600 237A49,49 0,0 1,600 139z"
+ android:strokeColor="#000" android:strokeWidth="3"/>
+ <path android:fillType="nonZero"
+ android:fillColor="#f00"
+ android:pathData="M950,81A107,107 0,0 1,950 295A107,107 0,0 1,950 81zM950,139A49,49 0,0 0,950 237A49,49 0,0 0,950 139z"
+ android:strokeColor="#000" android:strokeWidth="3"/>
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_1.xml
new file mode 100644
index 000000000..d67aca7cd
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_1.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+<group android:name="backgroundGroup"
+ android:scaleX="0.5"
+ android:scaleY="0.5">
+ <path
+ android:name="background1"
+ android:fillColor="@color/fill_gradient_linear"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="@color/fill_gradient_radial"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background3"
+ android:fillColor="@color/fill_gradient_sweep"
+ android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" />
+</group>
+<group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0" >
+ <path
+ android:name="twoLines"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0">
+ <path
+ android:name="twoLines1"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines3"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+
+ <group
+ android:name="translateGroupHalf"
+ android:translateX="65.0"
+ android:translateY="80.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="@string/twoLinePathData"
+ android:fillColor="@color/fill_gradient_linear"
+ android:strokeColor="@color/stroke_gradient"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+ </group>
+</group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_1_clamp.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_1_clamp.xml
new file mode 100644
index 000000000..2fa440a84
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_1_clamp.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+<group android:name="backgroundGroup"
+ android:scaleX="0.5"
+ android:scaleY="0.5">
+ <path
+ android:name="background1"
+ android:fillColor="@color/fill_gradient_linear_clamp"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="@color/fill_gradient_radial_clamp"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background3"
+ android:fillColor="@color/fill_gradient_sweep_clamp"
+ android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" />
+</group>
+<group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0" >
+ <path
+ android:name="twoLines"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_clamp"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0">
+ <path
+ android:name="twoLines1"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_clamp"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines3"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_clamp"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+
+ <group
+ android:name="translateGroupHalf"
+ android:translateX="65.0"
+ android:translateY="80.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="@string/twoLinePathData"
+ android:fillColor="@color/fill_gradient_linear_clamp"
+ android:strokeColor="@color/stroke_gradient_clamp"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+ </group>
+</group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_2.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_2.xml
new file mode 100644
index 000000000..abf3c7a86
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_2.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+<group android:name="backgroundGroup"
+ android:scaleX="0.5"
+ android:scaleY="0.5">
+ <path
+ android:name="background1"
+ android:fillColor="@color/fill_gradient_linear_item"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="@color/fill_gradient_radial_item"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background3"
+ android:fillColor="@color/fill_gradient_sweep_item"
+ android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" />
+</group>
+<group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0" >
+ <path
+ android:name="twoLines"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0">
+ <path
+ android:name="twoLines1"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines3"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+
+ <group
+ android:name="translateGroupHalf"
+ android:translateX="65.0"
+ android:translateY="80.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+ </group>
+</group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_2_repeat.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_2_repeat.xml
new file mode 100644
index 000000000..5a43f804a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_2_repeat.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+<group android:name="backgroundGroup"
+ android:scaleX="0.5"
+ android:scaleY="0.5">
+ <path
+ android:name="background1"
+ android:fillColor="@color/fill_gradient_linear_item_repeat"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="@color/fill_gradient_radial_item_repeat"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background3"
+ android:fillColor="@color/fill_gradient_sweep_item_repeat"
+ android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" />
+</group>
+<group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0" >
+ <path
+ android:name="twoLines"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_repeat"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0">
+ <path
+ android:name="twoLines1"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_repeat"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines3"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_repeat"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+
+ <group
+ android:name="translateGroupHalf"
+ android:translateX="65.0"
+ android:translateY="80.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_repeat"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+ </group>
+</group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_3.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_3.xml
new file mode 100644
index 000000000..5f9726f72
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_3.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+<group android:name="backgroundGroup"
+ android:scaleX="0.5"
+ android:scaleY="0.5">
+ <path
+ android:name="background1"
+ android:fillColor="@color/fill_gradient_linear_item_overlap"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="@color/fill_gradient_radial_item_short"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background3"
+ android:fillColor="@color/fill_gradient_sweep_item_long"
+ android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" />
+</group>
+<group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0" >
+ <path
+ android:name="twoLines"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_alpha"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0">
+ <path
+ android:name="twoLines1"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_alpha"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines3"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_alpha"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+
+ <group
+ android:name="translateGroupHalf"
+ android:translateX="65.0"
+ android:translateY="80.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_alpha"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+ </group>
+</group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_3_mirror.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_3_mirror.xml
new file mode 100644
index 000000000..e8de7c2b1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_gradient_3_mirror.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+<group android:name="backgroundGroup"
+ android:scaleX="0.5"
+ android:scaleY="0.5">
+ <path
+ android:name="background1"
+ android:fillColor="@color/fill_gradient_linear_item_overlap_mirror"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="@color/fill_gradient_radial_item_short_mirror"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background3"
+ android:fillColor="@color/fill_gradient_sweep_item_long_mirror"
+ android:pathData="M 400,400 l 200,0 l 0, 200 l -200, 0 z" />
+</group>
+<group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0" >
+ <path
+ android:name="twoLines"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_alpha_mirror"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0">
+ <path
+ android:name="twoLines1"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_alpha_mirror"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines3"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_alpha_mirror"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+
+ <group
+ android:name="translateGroupHalf"
+ android:translateX="65.0"
+ android:translateY="80.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="@string/twoLinePathData"
+ android:strokeColor="@color/stroke_gradient_item_alpha"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+ </group>
+</group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_group_clip.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_group_clip.xml
new file mode 100644
index 000000000..9574d7e52
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_group_clip.xml
@@ -0,0 +1,50 @@
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="48dp"
+ android:width="48dp"
+ android:viewportHeight="48"
+ android:viewportWidth="48">
+
+ <group>
+ <clip-path
+ android:name="clip1"
+ android:pathData="M 0, 0 l 48, 0 l 0, 30 l -48, 0 z"/>
+
+ <group>
+ <clip-path
+ android:name="clip2"
+ android:pathData="M 0, 18 l 48, 0 l 0, 30 l -48, 0 z"/>
+
+ <path
+ android:name="plus1"
+ android:pathData="M20 16h-4v8h-8v4h8v8h4v-8h8v-4h-8zm9-3.84v3.64l5-1v21.2h4v-26z"
+ android:fillColor="#ff00ff00"/>
+ </group>
+
+
+ <group android:name="backgroundGroup" >
+ <path
+ android:name="background1"
+ android:fillColor="#80000000"
+ android:pathData="M 0,0 l 24,0 l 0,24 l -24, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#80000000"
+ android:pathData="M 24,24 l 24,0 l 0, 24 l -24, 0 z" />
+ </group>
+ </group>
+</vector>
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_heart.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_heart.xml
new file mode 100644
index 000000000..ad991c9c3
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_heart.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24" >
+
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M16.0,5.0c-1.955.0 -3.83,1.268 -4.5,3.0c-0.67-1.732 -2.547-3.0 -4.5-3.0C4.4570007,5.0 2.5,6.931999 2.5,9.5c0.0,3.529 3.793,6.258 9.0,11.5c5.207-5.242 9.0-7.971 9.0-11.5C20.5,6.931999 18.543,5.0 16.0,5.0z" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_implicit_lineto.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_implicit_lineto.xml
new file mode 100644
index 000000000..d7b133b84
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_implicit_lineto.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="64"
+ android:viewportWidth="64" >
+
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="m0,0 32,0 0,32 -32,0 0,-32z" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_random_path_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_random_path_1.xml
new file mode 100644
index 000000000..5c55294a1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_random_path_1.xml
@@ -0,0 +1,49 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at"+
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="128"
+ android:viewportWidth="128" >
+
+ <path
+ android:fillColor="#FF00FF00"
+ android:pathData="
+ m 0.0 0.0
+ c 58.357853 57.648304 47.260395 2.2044754 3.0 3.0
+ s 61.29288 10.748665 6.0 6.0
+ s 0.12015152 45.193787 9.0 9.0
+ s 32.573513 46.862522 12.0 12.0
+ C 52.051823 62.050003 14.197739 51.99994 15.0 15.0
+ S 58.365482 51.877937 18.0 18.0
+ S 26.692455 3.9604378 21.0 21.0
+ S 21.433464 52.17514 24.0 24.0
+ M 27.0 27.0
+ s 0.77630234 20.606667 30.0 30.0
+ M 33.0 33.0
+ S 31.06879 21.506374 36.0 36.0
+ m 39.0 39.0
+ s 11.699013 23.684185 42.0 42.0
+ m 45.0 45.0
+ S 3.7642136 38.589584 48.0 48.0
+ Q 27.203026 53.329338 51.0 51.0
+ s 39.229023 15.1781845 54.0 54.0
+ Q 47.946877 23.706299 57.0 57.0
+ S 45.63452 56.15198 60.0 60.0 "
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="1" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_random_path_2.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_random_path_2.xml
new file mode 100644
index 000000000..95e0a546c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_random_path_2.xml
@@ -0,0 +1,49 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at"+
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="128"
+ android:viewportWidth="128" >
+
+ <path
+ android:fillColor="#FF00FF00"
+ android:pathData="
+ m 0.0 0.0
+ q 4.7088394 36.956432 3.0 3.0
+ s 29.470345 16.754963 6.0 6.0
+ q 20.278355 7.4670525 9.0 9.0
+ S 30.897224 17.732414 12.0 12.0
+ T 15.0 15.0
+ s 63.47204 45.67142 18.0 18.0
+ T 21.0 21.0
+ S 0.3184204 24.808247 24.0 24.0
+ t 27.0 27.0
+ s 39.02275 38.261158 30.0 30.0
+ t 33.0 33.0
+ S 50.709816 16.067192 36.0 36.0
+ a 62.50911 7.7131805 51.932335 0 0 39.0 39.0
+ s 5.155651 15.749123 42.0 42.0
+ a 51.87415 40.30564 49.804344 0 0 45.0 45.0
+ S 16.16534 62.55986 48.0 48.0
+ A 39.90161 43.904438 41.642593 1 0 51.0 51.0
+ s 46.258068 32.12831 54.0 54.0
+ A 22.962704 55.05604 42.912285 1 1 57.0 57.0
+ S 36.47731 54.216763 60.0 60.0 "
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="1" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_render_order_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_render_order_1.xml
new file mode 100644
index 000000000..d4472e2ec
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_render_order_1.xml
@@ -0,0 +1,89 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+ <group
+ android:name="FirstLevelGroup"
+ android:translateX="100.0"
+ android:translateY="0.0" >
+ <path
+ android:fillColor="#FFFF0000"
+ android:fillAlpha="0.9"
+ android:pathData="@string/rectangle200" />
+
+ <group
+ android:name="SecondLevelGroup1"
+ android:translateX="-100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillColor="#FF00FF00"
+ android:fillAlpha="0.81"
+ android:pathData="@string/rectangle200" />
+
+ <group
+ android:name="ThridLevelGroup1"
+ android:translateX="-100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillColor="#FF0000FF"
+ android:fillAlpha="0.729"
+ android:pathData="@string/rectangle200" />
+ </group>
+ <group
+ android:name="ThridLevelGroup2"
+ android:translateX="100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.72"
+ android:fillColor="#FF000000"
+ android:pathData="@string/rectangle200" />
+ </group>
+ </group>
+ <group
+ android:name="SecondLevelGroup2"
+ android:translateX="100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillColor="#FF0000FF"
+ android:fillAlpha="0.72"
+ android:pathData="@string/rectangle200" />
+
+ <group
+ android:name="ThridLevelGroup3"
+ android:translateX="-100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.648"
+ android:fillColor="#FFFF0000"
+ android:pathData="@string/rectangle200" />
+ </group>
+ <group
+ android:name="ThridLevelGroup4"
+ android:translateX="100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.576"
+ android:fillColor="#FF00FF00"
+ android:pathData="@string/rectangle200" />
+ </group>
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_render_order_2.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_render_order_2.xml
new file mode 100644
index 000000000..6fcb35520
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_render_order_2.xml
@@ -0,0 +1,89 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+ <group
+ android:name="FirstLevelGroup"
+ android:translateX="100.0"
+ android:translateY="0.0" >
+ <group
+ android:name="SecondLevelGroup1"
+ android:translateX="-100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.81"
+ android:fillColor="#FF00FF00"
+ android:pathData="@string/rectangle200" />
+
+ <group
+ android:name="ThridLevelGroup1"
+ android:translateX="-100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.729"
+ android:fillColor="#FF0000FF"
+ android:pathData="@string/rectangle200" />
+ </group>
+ <group
+ android:name="ThridLevelGroup2"
+ android:translateX="100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.648"
+ android:fillColor="#FF000000"
+ android:pathData="@string/rectangle200" />
+ </group>
+ </group>
+ <group
+ android:name="SecondLevelGroup2"
+ android:translateX="100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.72"
+ android:fillColor="#FF0000FF"
+ android:pathData="@string/rectangle200" />
+
+ <group
+ android:name="ThridLevelGroup3"
+ android:translateX="-100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.648"
+ android:fillColor="#FFFF0000"
+ android:pathData="@string/rectangle200" />
+ </group>
+ <group
+ android:name="ThridLevelGroup4"
+ android:translateX="100.0"
+ android:translateY="50.0" >
+ <path
+ android:fillAlpha="0.576"
+ android:fillColor="#FF00FF00"
+ android:pathData="@string/rectangle200" />
+ </group>
+ </group>
+
+ <path
+ android:fillAlpha="0.9"
+ android:fillColor="#FFFF0000"
+ android:pathData="@string/rectangle200" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_a_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_a_1.xml
new file mode 100644
index 000000000..e27464b7a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_a_1.xml
@@ -0,0 +1,43 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at"+
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="128"
+ android:viewportWidth="128" >
+
+ <path
+ android:fillColor="#FF00FF00"
+ android:pathData="m 45.712063 19.109837
+ H 24.509682
+ a 59.3415 26.877445 22.398209 1 1 3.3506432 1.6524277
+ a 34.922844 36.72583 13.569004 0 0 24.409462 20.931156
+ a 43.47134 32.61542 52.534607 1 0 7.187504 61.509724
+ A 30.621132 41.44202 50.885685 0 0 23.235489 26.638653
+ A 7.251148 15.767811 44.704533 1 1 19.989803 21.33052
+ A 55.645584 46.20288 19.40316 0 1 32.881298 53.410923
+ c 30.649612 4.8525085 21.96682 1.3304634 17.300182 14.747681
+ a 9.375069 44.365055 57.169727 0 0 56.01326 52.59596
+ A 50.071907 37.331825 56.301754 1 0 14.676102 62.04976
+ C 36.531925 4.6217957 47.59332 54.793385 13.562473 13.753647
+ A 2.3695297 42.578487 54.250687 0 1 33.1337 41.511288
+ a 39.4827 38.844944 54.52335 1 1 13.549484 46.81581
+ c 56.943657 51.96854 27.938824 61.148792 24.168636 46.642727
+ "
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="1" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_a_2.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_a_2.xml
new file mode 100644
index 000000000..924ba1bf8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_a_2.xml
@@ -0,0 +1,45 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at"+
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="128"
+ android:viewportWidth="128" >
+
+ <path
+ android:fillColor="#FF00FF00"
+ android:pathData="m 45.712063 19.109837
+ H 24.509682
+ A 37.689938 2.3916092 17.462616 1 0 24.958328 48.110596
+ q 45.248383 30.396336 5.777027 3.4086685
+ a 30.966236 62.67946 50.532032 1 0 29.213684 60.63014
+ L 56.16764 8.342098
+ Q 61.172253 1.4613304 4.4721107 38.287144
+ A 6.284897 22.991482 47.409508 1 1 44.10166 60.998764
+ t 36.36881 55.68292
+ a 51.938667 35.22107 22.272938 1 1 28.572739 60.848858
+ A 19.610851 11.569599 51.407906 1 1 56.82705 24.386292
+ T 36.918854 59.542286
+ a 33.191364 10.553429 53.047726 1 0 54.874985 7.409252
+ s 30.186714 42.154182 59.73551 35.50219
+ A 47.9379 5.776497 28.307701 1 1 3.3323975 30.113499
+ a 22.462494 28.096004 55.76455 0 0 25.58981 30.816948
+ S 43.91107 54.679676 19.540264 0.34284973
+ "
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="1" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_cq.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_cq.xml
new file mode 100644
index 000000000..e0848f0c1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_cq.xml
@@ -0,0 +1,42 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at"+
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="128"
+ android:viewportWidth="128" >
+
+ <path
+ android:fillColor="#FF00FF00"
+ android:pathData="m 30.81895 41.37989
+ v 31.00579
+ c 24.291603 52.03364 40.6086 24.840137 29.56704 6.5204926
+ 45.133224 22.913471 33.052887 21.727486 33.369 61.60278
+ 9.647232 22.098152 48.939598 47.470215 53.653687 62.32235
+ C 2.0560722 1.4615479 7.0928993 26.005287 40.137558 36.75628
+ 11.246731 32.178127 59.367462 60.34823 57.254383 37.357815
+ 47.75605 11.424667 3.3105545 51.886635 56.63027 17.12133
+ q 28.37534 32.85535 25.85654 33.57151
+ 10.356537 51.850616 54.085087 35.653175
+ 12.530029 52.87991 17.44696 11.780586
+ Q 2.585228 51.92801 60.000664 56.79912
+ 54.18275 51.500694 9.375679 23.836113
+ 60.35329 59.026245 31.058632 35.14934
+ "
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="1" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_st.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_st.xml
new file mode 100644
index 000000000..b104349a9
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_repeated_st.xml
@@ -0,0 +1,42 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at"+
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="128"
+ android:viewportWidth="128" >
+
+ <path
+ android:fillColor="#FF00FF00"
+ android:pathData="m 20.20005 8.139153
+ h 10.053165
+ s 14.2943 49.612846 35.520653 54.904068
+ 50.1405 17.044182 5.470337 40.180553
+ 3.125019 34.221123 53.212563 32.862965
+ S 35.985264 35.74349 0.15337753 59.27337
+ 2.2951508 44.56783 51.089413 29.829689
+ 8.5599785 22.649555 4.3914986 28.139206
+ t 11.932453 44.041077
+ 62.629326 7.40921
+ 23.302986 54.116184
+ T 43.560753 63.370514
+ 40.156204 17.60786
+ 40.12051 60.803394
+ "
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="1" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_1.xml
new file mode 100644
index 000000000..530c73b20
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_1.xml
@@ -0,0 +1,52 @@
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:viewportHeight="200"
+ android:viewportWidth="200"
+ android:width="64dp" >
+
+ <group>
+ <path
+ android:name="background1"
+ android:fillColor="#FF000000"
+ android:pathData="M 0,0 l 100,0 l 0, 100 l -100, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#FF000000"
+ android:pathData="M 100,100 l 100,0 l 0, 100 l -100, 0 z" />
+ </group>
+ <group
+ android:scaleX="-1"
+ android:scaleY="-1" >
+ <group
+ android:scaleX="-1"
+ android:scaleY="-1" >
+ <group
+ android:pivotX="100"
+ android:pivotY="100"
+ android:rotation="45" >
+ <path
+ android:name="twoLines"
+ android:fillColor="#FFFF0000"
+ android:pathData="M 100, 0 l 0, 100, -100, 0 z"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="10" />
+ </group>
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_2.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_2.xml
new file mode 100644
index 000000000..200eb617a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_2.xml
@@ -0,0 +1,48 @@
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:viewportHeight="200"
+ android:viewportWidth="200"
+ android:width="64dp" >
+
+ <group>
+ <path
+ android:name="background1"
+ android:fillColor="#FF000000"
+ android:pathData="M 0,0 l 100,0 l 0, 100 l -100, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#FF000000"
+ android:pathData="M 100,100 l 100,0 l 0, 100 l -100, 0 z" />
+ </group>
+ <group
+ android:scaleX="2"
+ android:scaleY="0.5" >
+ <group
+ android:pivotX="100"
+ android:pivotY="100"
+ android:rotation="45" >
+ <path
+ android:name="twoLines"
+ android:fillColor="#FFFF0000"
+ android:pathData="M 100, 0 l 0, 100, -100, 0 z"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="10" />
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_3.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_3.xml
new file mode 100644
index 000000000..74fa47521
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_scale_3.xml
@@ -0,0 +1,45 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:viewportHeight="200"
+ android:viewportWidth="200"
+ android:width="64dp" >
+
+ <group>
+ <path
+ android:name="background1"
+ android:fillColor="#FF000000"
+ android:pathData="M 0,0 l 100,0 l 0, 100 l -100, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#FF000000"
+ android:pathData="M 100,100 l 100,0 l 0, 100 l -100, 0 z" />
+ </group>
+ <group
+ android:scaleX="200"
+ android:scaleY="200" >
+ <group>
+ <path
+ android:name="twoLines"
+ android:fillColor="#FFFF0000"
+ android:pathData="M 0.75, 0.25 l 0, 0.5, -0.5, 0 z"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="0.05" />
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_schedule.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_schedule.xml
new file mode 100644
index 000000000..64d19e872
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_schedule.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24" >
+
+ <path
+ android:fillColor="#E6000000"
+ android:pathData="M11.994999,2.0C6.4679985,2.0 2.0,6.4780006 2.0,12.0s4.468,10.0 9.995,10.0S22.0,17.522 22.0,12.0S17.521,2.0 11.994999,2.0zM12.0,20.0c-4.42,0.0 -8.0-3.582-8.0-8.0s3.58-8.0 8.0-8.0s8.0,3.582 8.0,8.0S16.419998,20.0 12.0,20.0z" />
+ <path
+ android:fillColor="#E6000000"
+ android:pathData="M12.5,6.0l-1.5,0.0 0.0,7.0 5.3029995,3.1819992 0.75-1.249999-4.5529995-2.7320004z" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_settings.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_settings.xml
new file mode 100644
index 000000000..13d7f055c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_settings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24" >
+
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19.429,12.975998c0.042-0.32 0.07-0.645 0.07-0.976s-0.029-0.655-0.07-0.976l2.113-1.654c0.188-0.151 0.243-0.422 0.118-0.639l-2.0-3.463c-0.125-0.217-0.386-0.304-0.612-0.218l-2.49,1.004c-0.516-0.396-1.081-0.731-1.69-0.984l-0.375-2.648C14.456,2.1829987 14.25,2.0 14.0,2.0l-4.0,0.0C9.75,2.0 9.544,2.1829987 9.506,2.422001L9.131,5.0699997C8.521,5.322998 7.957,5.6570015 7.44,6.054001L4.952,5.0509987C4.726,4.965 4.464,5.052002 4.34,5.269001l-2.0,3.463C2.2150002,8.947998 2.27,9.219002 2.4580002,9.369999l2.112,1.653C4.528,11.344002 4.5,11.668999 4.5,12.0s0.029,0.656 0.071,0.977L2.4580002,14.630001c-0.188,0.151-0.243,0.422-0.118,0.639l2.0,3.463c0.125,0.217 0.386,0.304 0.612,0.218l2.489-1.004c0.516,0.396 1.081,0.731 1.69,0.984l0.375,2.648C9.544,21.817001 9.75,22.0 10.0,22.0l4.0,0.0c0.25,0.0 0.456-0.183 0.494-0.422l0.375-2.648c0.609-0.253 1.174-0.588 1.689-0.984l2.49,1.004c0.226,0.086 0.487-0.001 0.612-0.218l2.0-3.463c0.125-0.217 0.07-0.487-0.118-0.639L19.429,12.975998zM12.0,16.0c-2.21,0.0-4.0-1.791-4.0-4.0c0.0-2.21 1.79-4.0 4.0-4.0c2.208,0.0 4.0,1.79 4.0,4.0C16.0,14.209 14.208,16.0 12.0,16.0z" />
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_state_list.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_state_list.xml
new file mode 100644
index 000000000..65aa96766
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_state_list.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24" >
+
+ <path
+ android:fillColor="@color/vector_icon_fill_state_list"
+ android:strokeColor="@color/vector_icon_stroke_state_list"
+ android:strokeWidth="3"
+ android:pathData="M16.0,5.0c-1.955.0 -3.83,1.268 -4.5,3.0c-0.67-1.732 -2.547-3.0 -4.5-3.0C4.4570007,5.0 2.5,6.931999 2.5,9.5c0.0,3.529 3.793,6.258 9.0,11.5c5.207-5.242 9.0-7.971 9.0-11.5C20.5,6.931999 18.543,5.0 16.0,5.0z"/>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_state_list_2.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_state_list_2.xml
new file mode 100644
index 000000000..65aa96766
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_state_list_2.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24" >
+
+ <path
+ android:fillColor="@color/vector_icon_fill_state_list"
+ android:strokeColor="@color/vector_icon_stroke_state_list"
+ android:strokeWidth="3"
+ android:pathData="M16.0,5.0c-1.955.0 -3.83,1.268 -4.5,3.0c-0.67-1.732 -2.547-3.0 -4.5-3.0C4.4570007,5.0 2.5,6.931999 2.5,9.5c0.0,3.529 3.793,6.258 9.0,11.5c5.207-5.242 9.0-7.971 9.0-11.5C20.5,6.931999 18.543,5.0 16.0,5.0z"/>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_1.xml
new file mode 100644
index 000000000..af351f1dd
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_1.xml
@@ -0,0 +1,46 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:viewportHeight="200"
+ android:viewportWidth="200"
+ android:width="64dp" >
+
+ <group>
+ <path
+ android:name="background1"
+ android:fillColor="#FF000000"
+ android:pathData="M 0,0 l 100,0 l 0, 100 l -100, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#FF000000"
+ android:pathData="M 100,100 l 100,0 l 0, 100 l -100, 0 z" />
+ </group>
+ <group
+ android:translateX="50"
+ android:translateY="50" >
+ <path
+ android:name="twoLines"
+ android:pathData="M 100,20 l 0 80 l -30 -80"
+ android:fillColor="#FF000000"
+ android:strokeColor="#FF00FF00"
+ android:strokeLineCap="butt"
+ android:strokeLineJoin="miter"
+ android:strokeMiterLimit="6"
+ android:strokeWidth="20" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_2.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_2.xml
new file mode 100644
index 000000000..f85d5fc18
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_2.xml
@@ -0,0 +1,46 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:viewportHeight="200"
+ android:viewportWidth="200"
+ android:width="64dp" >
+
+ <group>
+ <path
+ android:name="background1"
+ android:fillColor="#FF000000"
+ android:pathData="M 0,0 l 100,0 l 0, 100 l -100, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#FF000000"
+ android:pathData="M 100,100 l 100,0 l 0, 100 l -100, 0 z" />
+ </group>
+ <group
+ android:translateX="50"
+ android:translateY="50" >
+ <path
+ android:name="twoLines"
+ android:pathData="M 100,20 l 0 80 l -30 -80"
+ android:fillColor="#FF000000"
+ android:strokeColor="#FF00FF00"
+ android:strokeLineCap="round"
+ android:strokeLineJoin="round"
+ android:strokeMiterLimit="10"
+ android:strokeWidth="20" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_3.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_3.xml
new file mode 100644
index 000000000..8f3d47ece
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_stroke_3.xml
@@ -0,0 +1,46 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:viewportHeight="200"
+ android:viewportWidth="200"
+ android:width="64dp" >
+
+ <group>
+ <path
+ android:name="background1"
+ android:fillColor="#FF000000"
+ android:pathData="M 0,0 l 100,0 l 0, 100 l -100, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#FF000000"
+ android:pathData="M 100,100 l 100,0 l 0, 100 l -100, 0 z" />
+ </group>
+ <group
+ android:translateX="50"
+ android:translateY="50" >
+ <path
+ android:name="twoLines"
+ android:pathData="M 100,20 l 0 80 l -30 -80"
+ android:fillColor="#FF000000"
+ android:strokeColor="#FF00FF00"
+ android:strokeLineCap="square"
+ android:strokeLineJoin="bevel"
+ android:strokeMiterLimit="10"
+ android:strokeWidth="20" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_1.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_1.xml
new file mode 100644
index 000000000..f6623d01d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_1.xml
@@ -0,0 +1,38 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="500"
+ android:viewportWidth="800" >
+
+ <group
+ android:pivotX="90"
+ android:pivotY="100"
+ android:rotation="20">
+ <path
+ android:name="pie2"
+ android:pathData="M200,350 l 50,-25
+ a25,12 -30 0,1 100,-50 l 50,-25
+ a25,25 -30 0,1 100,-50 l 50,-25
+ a25,37 -30 0,1 100,-50 l 50,-25
+ a25,50 -30 0,1 100,-50 l 50,-25"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="10" />
+ </group>
+
+</vector>
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_2.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_2.xml
new file mode 100644
index 000000000..87da0bbee
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_2.xml
@@ -0,0 +1,48 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="200"
+ android:viewportWidth="200" >
+
+ <group>
+ <path
+ android:name="background1"
+ android:pathData="M 0,0 l 100,0 l 0, 100 l -100, 0 z"
+ android:fillColor="#FF000000"/>
+ <path
+ android:name="background2"
+ android:pathData="M 100,100 l 100,0 l 0, 100 l -100, 0 z"
+ android:fillColor="#FF000000"/>
+ </group>
+ <group
+ android:pivotX="100"
+ android:pivotY="100"
+ android:rotation="90"
+ android:scaleX="0.75"
+ android:scaleY="0.5"
+ android:translateX="0.0"
+ android:translateY="100.0">
+ <path
+ android:name="twoLines"
+ android:pathData="M 100,10 v 90 M 10,100 h 90"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="10" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_3.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_3.xml
new file mode 100644
index 000000000..fc30af3e9
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_3.xml
@@ -0,0 +1,48 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="200"
+ android:viewportWidth="200" >
+
+ <group>
+ <path
+ android:name="background1"
+ android:pathData="M 0,0 l 100,0 l 0, 100 l -100, 0 z"
+ android:fillColor="#FF000000"/>
+ <path
+ android:name="background2"
+ android:pathData="M 100,100 l 100,0 l 0, 100 l -100, 0 z"
+ android:fillColor="#FF000000"/>
+ </group>
+ <group
+ android:pivotX="0"
+ android:pivotY="0"
+ android:rotation="90"
+ android:scaleX="0.75"
+ android:scaleY="0.5"
+ android:translateX="100.0"
+ android:translateY="100.0">
+ <path
+ android:name="twoLines"
+ android:pathData="M 100,10 v 90 M 10,100 h 90"
+ android:fillColor="#00000000"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="10" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_4.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_4.xml
new file mode 100644
index 000000000..5b40d0d07
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_4.xml
@@ -0,0 +1,68 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+ <group android:name="backgroundGroup" >
+ <path
+ android:name="background1"
+ android:fillColor="#80000000"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#80000000"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ </group>
+ <group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0" >
+ <path
+ android:name="twoLines"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FFFF0000"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0" >
+ <path
+ android:name="twoLines1"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0" >
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_5.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_5.xml
new file mode 100644
index 000000000..4a27754d1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_5.xml
@@ -0,0 +1,81 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400" >
+
+ <group android:name="backgroundGroup" >
+ <path
+ android:name="background1"
+ android:fillColor="#80000000"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#80000000"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ </group>
+ <group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0" >
+ <path
+ android:name="twoLines"
+ android:pathData="M 0,0 v 150 M 0,0 h 150"
+ android:strokeColor="#FFFF0000"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0" >
+ <path
+ android:name="twoLines1"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0" >
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines3"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+
+ <group
+ android:name="translateGroupHalf"
+ android:translateX="65.0"
+ android:translateY="80.0" >
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="20" />
+ </group>
+ </group>
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_6.xml b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_6.xml
new file mode 100644
index 000000000..2c174fb00
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/vector_icon_transformation_6.xml
@@ -0,0 +1,85 @@
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="64dp"
+ android:width="64dp"
+ android:viewportHeight="400"
+ android:viewportWidth="400"
+ android:alpha = "0.5" >
+
+ <group android:name="backgroundGroup">
+ <path
+ android:name="background1"
+ android:fillColor="#FF000000"
+ android:pathData="M 0,0 l 200,0 l 0, 200 l -200, 0 z" />
+ <path
+ android:name="background2"
+ android:fillColor="#FF000000"
+ android:pathData="M 200,200 l 200,0 l 0, 200 l -200, 0 z" />
+ </group>
+ <group
+ android:name="translateToCenterGroup"
+ android:translateX="50.0"
+ android:translateY="90.0">
+ <path
+ android:name="twoLines"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FFFF0000"
+ android:strokeWidth="20" />
+
+ <group
+ android:name="rotationGroup"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:rotation="-45.0" >
+ <path
+ android:name="twoLines1"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FF00FF00"
+ android:strokeWidth="20"
+ android:strokeAlpha="0.5" />
+
+ <group
+ android:name="translateGroup"
+ android:translateX="130.0"
+ android:translateY="160.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines3"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="20"
+ android:strokeAlpha="0.25" />
+ </group>
+ </group>
+
+ <group
+ android:name="translateGroupHalf"
+ android:translateX="65.0"
+ android:translateY="80.0">
+ <group android:name="scaleGroup" >
+ <path
+ android:name="twoLines2"
+ android:pathData="M 0,0 v 100 M 0,0 h 100"
+ android:strokeColor="#FF0000FF"
+ android:strokeWidth="20"
+ android:strokeAlpha="0.5" />
+ </group>
+ </group>
+ </group>
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/drawable/webp_test.webp b/integration_tests/nativegraphics/src/main/res/drawable/webp_test.webp
new file mode 100644
index 000000000..7b1009f0d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/drawable/webp_test.webp
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/font/a3em.ttf b/integration_tests/nativegraphics/src/main/res/font/a3em.ttf
new file mode 100644
index 000000000..e7814dbea
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/font/a3em.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/font/b3em.ttf b/integration_tests/nativegraphics/src/main/res/font/b3em.ttf
new file mode 100644
index 000000000..63948a22c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/font/b3em.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/font/c3em.ttf b/integration_tests/nativegraphics/src/main/res/font/c3em.ttf
new file mode 100644
index 000000000..badc3e298
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/font/c3em.ttf
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/font/multistyle_family.xml b/integration_tests/nativegraphics/src/main/res/font/multistyle_family.xml
new file mode 100644
index 000000000..2522e7279
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/font/multistyle_family.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<font-family xmlns:android="http://schemas.android.com/apk/res/android">
+ <font android:font="@font/a3em" android:fontWeight="400" android:fontStyle="normal" />
+ <font android:font="@font/b3em" android:fontWeight="400" android:fontStyle="italic" />
+ <font android:font="@font/c3em" android:fontWeight="700" android:fontStyle="italic" />
+</font-family>
diff --git a/integration_tests/nativegraphics/src/main/res/font/multiweight_family.xml b/integration_tests/nativegraphics/src/main/res/font/multiweight_family.xml
new file mode 100644
index 000000000..2ed049021
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/font/multiweight_family.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<font-family xmlns:android="http://schemas.android.com/apk/res/android">
+ <font android:font="@font/a3em" android:fontWeight="100" android:fontStyle="normal" />
+ <font android:font="@font/b3em" android:fontWeight="400" android:fontStyle="normal" />
+ <font android:font="@font/c3em" android:fontWeight="700" android:fontStyle="normal" />
+</font-family>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/blue_padded_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/blue_padded_layout.xml
new file mode 100644
index 000000000..2bfd0499c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/blue_padded_layout.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"
+ android:clipChildren="false">
+ <android.uirendering.cts.testclasses.view.UnclippedBlueView
+ android:id="@+id/child"
+ android:layout_width="80px"
+ android:layout_height="80px"
+ android:paddingLeft="15px"
+ android:paddingTop="16px"
+ android:paddingRight="17px"
+ android:paddingBottom="18px"/>
+</FrameLayout>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/blue_padded_square.xml b/integration_tests/nativegraphics/src/main/res/layout/blue_padded_square.xml
new file mode 100644
index 000000000..7d867d98a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/blue_padded_square.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"
+ android:background="@drawable/blue_padded_square"/>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/circle_clipped_webview.xml b/integration_tests/nativegraphics/src/main/res/layout/circle_clipped_webview.xml
new file mode 100644
index 000000000..49add8868
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/circle_clipped_webview.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<android.uirendering.cts.testclasses.view.CircleClipFrameLayout
+ android:id="@+id/circle_clip_frame_layout"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height">
+ <WebView
+ android:id="@+id/webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+</android.uirendering.cts.testclasses.view.CircleClipFrameLayout>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/force_dark_siblings.xml b/integration_tests/nativegraphics/src/main/res/layout/force_dark_siblings.xml
new file mode 100644
index 000000000..6e25452bf
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/force_dark_siblings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height">
+
+ <android.uirendering.cts.testinfrastructure.CanvasClientView
+ android:id="@+id/bg_canvas"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"/>
+
+ <android.uirendering.cts.testinfrastructure.CanvasClientView
+ android:id="@+id/fg_canvas"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/layout/frame_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/frame_layout.xml
new file mode 100644
index 000000000..4ceac5d92
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/frame_layout.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/frame_layout"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"/>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/frame_layout_webview.xml b/integration_tests/nativegraphics/src/main/res/layout/frame_layout_webview.xml
new file mode 100644
index 000000000..a3afd81e6
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/frame_layout_webview.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/frame_layout"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height">
+ <include layout="@layout/test_content_webview"/>
+</FrameLayout>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/simple_force_dark.xml b/integration_tests/nativegraphics/src/main/res/layout/simple_force_dark.xml
new file mode 100644
index 000000000..f042f8765
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/simple_force_dark.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<android.uirendering.cts.testinfrastructure.CanvasClientView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/canvas"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"
+ android:background="@android:color/white" /> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/layout/simple_rect_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/simple_rect_layout.xml
new file mode 100644
index 000000000..7d6f9280b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/simple_rect_layout.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height">
+ <View android:layout_width="80px"
+ android:layout_height="80px"
+ android:translationX="5px"
+ android:translationY="5px"
+ android:background="#00f" />
+</FrameLayout>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/simple_red_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/simple_red_layout.xml
new file mode 100644
index 000000000..1c551d30d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/simple_red_layout.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"
+ android:background="#f00"/>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/simple_shadow_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/simple_shadow_layout.xml
new file mode 100644
index 000000000..042c2a9e1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/simple_shadow_layout.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height">
+ <View android:id="@+id/shadow_view"
+ android:layout_width="40px"
+ android:layout_height="40px"
+ android:translationX="25px"
+ android:translationY="25px"
+ android:elevation="10dp"
+ android:background="#fff" />
+</FrameLayout> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/layout/simple_white_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/simple_white_layout.xml
new file mode 100644
index 000000000..43a10f5e1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/simple_white_layout.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"
+ android:background="@android:color/white"/>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/skew_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/skew_layout.xml
new file mode 100644
index 000000000..0b67a8f3a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/skew_layout.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<android.uirendering.cts.testclasses.view.SkewLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height">
+ <View
+ android:id="@+id/view1"
+ android:layout_width="50dp"
+ android:layout_height="50dp"
+ android:background="#F00"
+ android:elevation="8dp" />
+</android.uirendering.cts.testclasses.view.SkewLayout>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/test_container.xml b/integration_tests/nativegraphics/src/main/res/layout/test_container.xml
new file mode 100644
index 000000000..fd7b4e29e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/test_container.xml
@@ -0,0 +1,24 @@
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <FrameLayout
+ android:layout_gravity="center"
+ android:id="@+id/test_content_wrapper"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height">
+ </FrameLayout>
+</FrameLayout>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/test_content_webview.xml b/integration_tests/nativegraphics/src/main/res/layout/test_content_webview.xml
new file mode 100644
index 000000000..32762ec44
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/test_content_webview.xml
@@ -0,0 +1,19 @@
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<WebView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/webview"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"/> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/layout/textureview.xml b/integration_tests/nativegraphics/src/main/res/layout/textureview.xml
new file mode 100644
index 000000000..f2fbfd16d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/textureview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<TextureView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height" />
diff --git a/integration_tests/nativegraphics/src/main/res/layout/vector_drawable_scale_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/vector_drawable_scale_layout.xml
new file mode 100644
index 000000000..2cf30a23f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/vector_drawable_scale_layout.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/scaletest">
+
+ <ImageView
+ android:id="@+id/imageview1"
+ android:layout_width="64px"
+ android:layout_height="64px"/>
+
+ <ImageView
+ android:id="@+id/imageview2"
+ android:layout_width="4px"
+ android:layout_height="4px"/>
+</LinearLayout>
+
diff --git a/integration_tests/nativegraphics/src/main/res/layout/viewpropertyanimator_test_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/viewpropertyanimator_test_layout.xml
new file mode 100644
index 000000000..b6f67d80e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/viewpropertyanimator_test_layout.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:background="#FFFFFF"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <FrameLayout
+ android:id="@+id/viewalpha_test_container"
+ android:background="#0000FF"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <android.uirendering.cts.testclasses.view.AlphaTestView
+ android:id="@+id/alpha_test_view"
+ android:layout_width="100px"
+ android:layout_height="100px"
+ />
+ </FrameLayout>
+</FrameLayout>
diff --git a/integration_tests/nativegraphics/src/main/res/layout/webview_canvas_rrect_clip.xml b/integration_tests/nativegraphics/src/main/res/layout/webview_canvas_rrect_clip.xml
new file mode 100644
index 000000000..ff1e3a1f2
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/webview_canvas_rrect_clip.xml
@@ -0,0 +1,19 @@
+<!-- Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<android.uirendering.cts.testclasses.view.WebviewCanvasRRectClip
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/webview_canvas_rrect_clip"
+ android:layout_width="@dimen/test_width"
+ android:layout_height="@dimen/test_height"/> \ No newline at end of file
diff --git a/integration_tests/nativegraphics/src/main/res/layout/wide_gamut_bitmap_layout.xml b/integration_tests/nativegraphics/src/main/res/layout/wide_gamut_bitmap_layout.xml
new file mode 100644
index 000000000..adc436ae0
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/layout/wide_gamut_bitmap_layout.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<android.uirendering.cts.testclasses.view.BitmapView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
diff --git a/integration_tests/nativegraphics/src/main/res/raw/sample_1mp.dng b/integration_tests/nativegraphics/src/main/res/raw/sample_1mp.dng
new file mode 100644
index 000000000..c1c10787c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/raw/sample_1mp.dng
Binary files differ
diff --git a/integration_tests/nativegraphics/src/main/res/values-television/dimens.xml b/integration_tests/nativegraphics/src/main/res/values-television/dimens.xml
new file mode 100644
index 000000000..6d5b6d192
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/values-television/dimens.xml
@@ -0,0 +1,18 @@
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <item type="dimen" format="float" name="expected_ambient_shadow_alpha">0.15</item>
+ <item type="dimen" format="float" name="expected_spot_shadow_alpha">0.3</item>
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/values/arrays.xml b/integration_tests/nativegraphics/src/main/res/values/arrays.xml
new file mode 100644
index 000000000..71e013317
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/values/arrays.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<resources>
+ <item type="integer" name="reference" format="integer">101</item>
+
+ <string-array name="strings">
+ <item>zero</item>
+ <item>1</item>
+ <item>@string/reference</item>
+ </string-array>
+
+ <integer-array name="integers">
+ <item>0</item>
+ <item>1</item>
+ <item>@integer/reference</item>
+ </integer-array>
+
+ <array name="difficultyLevel">
+ <item>Easy</item>
+ <item>Medium</item>
+ <item>Hard</item>
+ </array>
+
+ <string-array name="string">
+ <item>Test String 1</item>
+ <item>Test String 2</item>
+ <item>Test String 3</item>
+ </string-array>
+
+ <integer-array name="table_row_layout">
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+ <item>7</item>
+ <item>8</item>
+ <item>9</item>
+ <item>10</item>
+ </integer-array>
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/values/attrs.xml b/integration_tests/nativegraphics/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..dbbf3e21b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/values/attrs.xml
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<resources>
+ <declare-styleable name="Style1">
+ <attr name="Type1" format="integer">
+ <enum name="type" value="28" />
+ <enum name="data" value="0xff00ff00" />
+ <enum name="asset_cookie" value="0" />
+ <enum name="resource_id" value="0" />
+ <enum name="changing_config" value="0" />
+ </attr>
+ <attr name="Type2" format="integer">
+ <enum name="type" value="28" />
+ <enum name="data" value="0xff0000ff" />
+ <enum name="asset_cookie" value="0" />
+ <enum name="resource_id" value="0" />
+ <enum name="changing_config" value="0" />
+ </attr>
+ </declare-styleable>
+ <attr name="type1" format="boolean"/>
+ <attr name="type2" format="boolean"/>
+ <attr name="type3" format="color"/>
+ <attr name="type4" format="reference|color"/>
+ <attr name="type5" format="dimension"/>
+ <attr name="type6" format="dimension"/>
+ <attr name="type7" format="dimension"/>
+ <attr name="type8" format="reference"/>
+ <attr name="type9" format="float"/>
+ <attr name="type10" format="fraction"/>
+ <attr name="type11" format="integer"/>
+ <attr name="type12" format="integer"/>
+ <attr name="type13" format="reference|string"/>
+ <attr name="type14" format="string"/>
+ <attr name="type15" format="reference"/>
+ <attr name="type16" format="string"/>
+ <declare-styleable name="style1">
+ <attr name="type1"/>
+ <attr name="type2"/>
+ <attr name="type3"/>
+ <attr name="type4"/>
+ <attr name="type5"/>
+ <attr name="type6"/>
+ <attr name="type7"/>
+ <attr name="type8"/>
+ <attr name="type9"/>
+ <attr name="type10"/>
+ <attr name="type11"/>
+ <attr name="type12"/>
+ <attr name="type13"/>
+ <attr name="type14"/>
+ <attr name="type15"/>
+ <attr name="type16"/>
+ </declare-styleable>
+ <attr name="testEnum">
+ <enum name="val1" value="1" />
+ <enum name="val2" value="2" />
+ <enum name="val10" value="10" />
+ </attr>
+ <attr name="testFlags">
+ <flag name="bit1" value="0x1" />
+ <flag name="bit2" value="0x2" />
+ <flag name="bit31" value="0x40000000" />
+ </attr>
+ <attr name="testString" format="string" />
+ <declare-styleable name="EnumStyle">
+ <attr name="testEnum" />
+ </declare-styleable>
+ <declare-styleable name="FlagStyle">
+ <attr name="testFlags" />
+ </declare-styleable>
+ <declare-styleable name="TestConfig">
+ <attr name="testString" />
+ </declare-styleable>
+ <!-- Size of text. Recommended dimension type for text is "sp" for scaled-pixels (example: 15sp).
+ Supported values include the following:<p/>
+ <ul>
+ <li><b>px</b> Pixels</li>
+ <li><b>sp</b> Scaled pixels (scaled to relative pixel size on screen). See {@link android.util.DisplayMetrics} for more information.</li>
+ <li><b>pt</b> Points</li>
+ <li><b>dip</b> Device independent pixels. See {@link android.util.DisplayMetrics} for more information.</li>
+ </ul>
+ -->
+ <attr name="textSize" format="dimension" />
+ <attr name="typeface">
+ <enum name="normal" value="0" />
+ <enum name="sans" value="1" />
+ <enum name="serif" value="2" />
+ <enum name="monospace" value="3" />
+ </attr>
+ <!-- Default text typeface style. -->
+ <attr name="textStyle">
+ <flag name="normal" value="0" />
+ <flag name="bold" value="1" />
+ <flag name="italic" value="2" />
+ </attr>
+ <!-- Color of text (usually same as colorForeground). -->
+ <attr name="textColor" format="reference|color" />
+ <!-- Color of highlighted text. -->
+ <attr name="textColorHighlight" format="reference|color" />
+ <!-- Color of hint text (displayed when the field is empty). -->
+ <attr name="textColorHint" format="reference|color" />
+ <!-- Color of link text (URLs). -->
+ <attr name="textColorLink" format="reference|color" />
+ <declare-styleable name="TextAppearance">
+ <attr name="textColor"/>
+ <attr name="textSize"/>
+ <attr name="textStyle"/>
+ <attr name="typeface"/>
+ <attr name="textColorHighlight"/>
+ <attr name="textColorHint"/>
+ <attr name="textColorLink"/>
+ </declare-styleable>
+ <!-- Integer used to uniquely identify theme overrides. -->
+ <attr name="themeType" format="integer"/>
+ <!-- Theme reference used to override parent theme. -->
+ <attr name="themeOverrideAttr" format="reference"/>
+
+ <!-- Drawable theming attributes -->
+ <attr name="themeBoolean" />
+ <attr name="themeColor" />
+ <attr name="themeFloat" />
+ <attr name="themeInteger" />
+ <attr name="themeDimension" />
+ <attr name="themeDrawable" />
+ <attr name="themeBitmap" />
+ <attr name="themeNinePatch" />
+ <attr name="themeGravity" />
+ <attr name="themeTileMode" />
+ <attr name="themeAngle" />
+ <attr name="themeVectorDrawableFillColor" />
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/values/colors.xml b/integration_tests/nativegraphics/src/main/res/values/colors.xml
new file mode 100644
index 000000000..64f1589b7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/values/colors.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<resources>
+ <drawable name="red">#7f00</drawable>
+ <drawable name="blue">#770000ff</drawable>
+ <drawable name="black">#77ffffff</drawable>
+ <drawable name="yellow">#77ffff00</drawable>
+ <color name="testcolor1">#ff00ff00</color>
+ <color name="testcolor2">#ffff0000</color>
+ <color name="failColor">#ff0000ff</color>
+ <color name="colorPrimary">#008577</color>
+ <color name="colorPrimaryDark">#00574B</color>
+ <color name="colorAccent">#D81B60</color>
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/values/dimens.xml b/integration_tests/nativegraphics/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..422861152
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/values/dimens.xml
@@ -0,0 +1,21 @@
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <dimen name="test_width">90px</dimen>
+ <dimen name="test_height">90px</dimen>
+
+ <item type="dimen" format="float" name="expected_ambient_shadow_alpha">0.039</item>
+ <item type="dimen" format="float" name="expected_spot_shadow_alpha">0.19</item>
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/values/strings.xml b/integration_tests/nativegraphics/src/main/res/values/strings.xml
new file mode 100644
index 000000000..e599d8a27
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/values/strings.xml
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="permlab_testGranted">Test Granted</string>
+ <string name="permdesc_testGranted">Used for running CTS tests, for testing operations
+ where we have the permission.</string>
+ <string name="permlab_testDynamic">Test Dynamic</string>
+ <string name="permdesc_testDynamic">Used for running CTS tests, for testing adding
+ dynamic permissions.</string>
+ <string name="permlab_testDenied">Test Denied</string>
+ <string name="permdesc_testDenied">Used for running CTS tests, for testing operations
+ where we do not have the permission.</string>
+ <string name="explain">1. click start. \n2. if above image shaked, then press pass button,
+ else press failed button.</string>
+ <string name="text_view_hello">Hello! Text view!</string>
+ <string name="text_view_hello_two_lines">Hello! \nText view!</string>
+ <string name="text_view_simple_hint">This is a hint.</string>
+ <string name="text_view_hint">This is a string for testing hint of textview.</string>
+ <string name="activity_forwarding">App/Forwarding</string>
+ <string name="forwarding">$$$</string>
+ <string name="go">Go</string>
+ <string name="back">Back</string>
+ <string name="forward_target">
+ Press back button and notice we don\'t see the previous activity.
+ </string>
+ <string name="edit_text">testing</string>
+ <string name="text">DialogTest</string>
+ <string name="text_country">Country</string>
+ <string name="text_name">Name</string>
+ <string name="hello_world">Hello, World!</string>
+ <string name="hello_android">Hello, Android!</string>
+ <string name="alert_dialog_username">Name:</string>
+ <string name="alert_dialog_password">Password:</string>
+ <string name="alert_dialog_positive">Positive</string>
+ <string name="alert_dialog_negative">Negative</string>
+ <string name="alert_dialog_neutral">Neutral</string>
+ <string name="notify">Notify </string>
+ <string name="tabs_1">testing</string>
+ <string name="table_layout_first">first</string>
+ <string name="table_layout_second">second</string>
+ <string name="table_layout_third">third</string>
+ <string name="table_layout_long">Very long to make the string out of the screen</string>
+ <string name="chronometer_text">Test Chronometer</string>
+ <string name="am">AM</string>
+ <string name="pm">PM</string>
+ <string name="viewgroup_test">ViewGroup test</string>
+ <string name="viewanimator_test">ViewAnimator test</string>
+ <string name="id_ok">OK</string>
+ <string name="id_cancel">Cancel</string>
+ <string name="context_test_string1">This is %s string.</string>
+ <string name="context_test_string2">This is test string.</string>
+ <string name="animationutils_test_instructions">Choose different animations</string>
+ <string name="animationutils_test_alpha">Alpha animation</string>
+ <string name="animationutils_test_scale">Scale animation</string>
+ <string name="animationutils_test_rotate">Rotate animation</string>
+ <string name="animationutils_test_translate">Translate animation</string>
+ <string name="animationutils_test_set">Animation set</string>
+ <string name="animationutils_test_layout">Layout animation</string>
+ <string name="animationutils_test_gridlayout">Grid layout animation</string>
+ <string name="twolinelistitem_test_text1">text1</string>
+ <string name="twolinelistitem_test_text2">text2</string>
+ <string name="metadata_text">metadata text</string>
+ <string name="horizontal_text_1">horizontal 1</string>
+ <string name="horizontal_text_2">horizontal 2</string>
+ <string name="horizontal_text_3">horizontal 3</string>
+ <string name="vertical_text_1">vertical 1</string>
+ <string name="vertical_text_2">vertical 2</string>
+ <string name="vertical_text_3">vertical 3</string>
+ <string name="reference">here</string>
+ <string name="coerceIntegerToString">100</string>
+ <string name="coerceBooleanToString">true</string>
+ <string name="coerceColorToString">#fff</string>
+ <string name="coerceFloatToString">100.0</string>
+ <string name="coerceDimensionToString">100px</string>
+ <string name="coerceFractionToString">100<xliff:g id="percent">%</xliff:g></string>
+ <string name="formattedStringNone">Format[]</string>
+ <string name="formattedStringOne">Format[<xliff:g id="format">%d</xliff:g>]</string>
+ <string name="formattedStringTwo">Format[<xliff:g id="format">%3$d,%2$s</xliff:g>]</string>
+ <string name="checkboxpref_key">checkboxpref_key</string>
+ <string name="checkboxpref_title">title of preference</string>
+ <string name="checkboxpref_summary">summary of preference</string>
+ <string name="checkboxpref_summary_on">summary on of preference</string>
+ <string name="checkboxpref_summary_off">summary off of preference</string>
+ <string name="checkboxpref_depend">checkboxpref_depend</string>
+ <string name="checkboxpref_depend_title"> depend title of preference</string>
+ <string name="checkboxpref_depend_summary"> depend summary of preference</string>
+ <string name="edittextpref_key">edittextpref_key</string>
+ <string name="edittextpref_default_value">default value of preference</string>
+ <string name="edittextpref_title">title of edit text preference</string>
+ <string name="edittextpref_summary">summary of edit text preference</string>
+ <string name="edittextpref_dialog_title">dialog title of edit text preference</string>
+ <string name="edittextpref_text">text of edit text preference</string>
+ <string name="listpref_key">listpref_key</string>
+ <string name="listpref_title">title of list preference</string>
+ <string name="listpref_summary">summary of list preference</string>
+ <string name="listpref_dialogtitle">dialog title of list preference</string>
+ <string name="easy">Easy</string>
+ <string name="medium">Medium</string>
+ <string name="hard">Hard</string>
+ <string name="footer_view">Footer view</string>
+ <string name="header_view">Header view</string>
+ <string name="dialogpref_title">title of dialog preference </string>
+ <string name="dialogpref_dialog_title">dialog title of dialog preference </string>
+ <string name="dialogpref_key">dialogpref_key</string>
+ <string name="dialogpref_default_value">default value of dialog preference</string>
+ <string name="dialogpref_summary">summary of dialog preference</string>
+ <string name="dialogpref_message">message of dialog preference</string>
+ <string name="dialogpref_sure">Sure</string>
+ <string name="dialogpref_cancel">Cancel</string>
+ <string name="pref_key">pref_key</string>
+ <string name="pref_title">title of preference</string>
+ <string name="pref_summary">summary of preference</string>
+ <string name="pref_depend_key">pref_depend_key</string>
+ <string name="pref_depend_title"> depend title of preference</string>
+ <string name="pref_depend_summary"> depend summary of preference</string>
+ <string name="android_intent_action_preference">android.intent.action.PREFERENCE</string>
+ <string name="def_pref_key">def_pref_key</string>
+ <string name="def_pref_title">default preference</string>
+ <string name="def_pref_summary">This is default preference of cts</string>
+ <string name="relative_view1">view 1</string>
+ <string name="relative_view2">view 2</string>
+ <string name="relative_view3">view 3</string>
+ <string name="relative_view4">view 4</string>
+ <string name="relative_view5">view 5</string>
+ <string name="relative_view6">view 6</string>
+ <string name="relative_view7">view 7</string>
+ <string name="relative_view8">view 8</string>
+ <string name="relative_view9">view 9</string>
+ <string name="relative_view10">view 10</string>
+ <string name="relative_view11">view 11</string>
+ <string name="relative_view12">view 12</string>
+ <string name="relative_view13">view 13</string>
+ <string name="country">Country:</string>
+ <string name="symbol">Symbol:</string>
+ <string name="country_warning">No such country registered</string>
+ <string name="version_cur">base</string>
+ <string name="version_old">base</string>
+ <string name="version_v3">base</string>
+ <string name="authenticator_label">Android CTS</string>
+ <string name="search_label">Android CTS</string>
+ <string name="tag1">tag 1</string>
+ <string name="tag2">tag 2</string>
+
+ <string name="button">Button</string>
+ <string name="holo_test">Holo Test</string>
+ <string name="holo_generator">Holo Generator</string>
+ <string name="holo_light_test">Holo Light Test</string>
+ <string name="holo_light_generator">Holo Light Generator</string>
+ <string name="reference_image">Reference Image: </string>
+ <string name="generated_image">Generated Image: </string>
+ <string name="themes_prompt">Select a Theme:</string>
+ <string name="sample_text">Sample text goes here. I wanted something creative and whimsical
+but then I just got bored...</string>
+ <string name="long_text">This is a really long string which exceeds the width of the view.
+New devices have a much larger screen which actually enables long strings to be displayed
+with no fading. I have made this string longer to fix this case. If you are correcting this
+text, I would love to see the kind of devices you guys now use! Guys, maybe some devices need longer string!
+I think so, so how about double this string, like copy and paste!
+This is a really long string which exceeds the width of the view.
+New devices have a much larger screen which actually enables long strings to be displayed
+with no fading. I have made this string longer to fix this case. If you are correcting this
+text, I would love to see the kind of devices you guys now use! Guys, maybe some devices need longer string!
+I think so, so how about double this string, like copy and paste! </string>
+ <string name="rectangle200">"M 0,0 l 200,0 l 0, 200 l -200, 0 z"</string>
+ <string name="twoLinePathData">"M 0,0 v 100 M 0,0 h 100"</string>
+ <string name="round_box">"m2.10001,-6c-1.9551,0 -0.5,0.02499 -2.10001,0.02499c-1.575,0 0.0031,-0.02499 -1.95,-0.02499c-2.543,0 -4,2.2816 -4,4.85001c0,3.52929 0.25,6.25 5.95,6.25c5.7,0 6,-2.72071 6,-6.25c0,-2.56841 -1.35699,-4.85001 -3.89999,-4.85001"</string>
+ <string name="heart">"m4.5,-7c-1.95509,0 -3.83009,1.26759 -4.5,3c-0.66991,-1.73241 -2.54691,-3 -4.5,-3c-2.543,0 -4.5,1.93159 -4.5,4.5c0,3.5293 3.793,6.2578 9,11.5c5.207,-5.2422 9,-7.9707 9,-11.5c0,-2.56841 -1.957,-4.5 -4.5,-4.5"</string>
+ <string name="last_screenshot">last_screenshot</string>
+ <string name="current_screenshot">current_screenshot</string>
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/values/styles.xml b/integration_tests/nativegraphics/src/main/res/values/styles.xml
new file mode 100644
index 000000000..7e05f0ab5
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/values/styles.xml
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="Whatever">
+ <item name="type1">true</item>
+ <item name="type2">false</item>
+ <item name="type3">#ff0000ff</item>
+ <item name="type4">#ff00ff00</item>
+ <item name="type5">0.75px</item>
+ <item name="type6">10px</item>
+ <item name="type7">18px</item>
+ <item name="type8">@drawable/pass</item>
+ <item name="type9">3.14</item>
+ <item name="type10">100%</item>
+ <item name="type11">365</item>
+ <item name="type12">86400</item>
+ <item name="type13">@string/hello_android</item>
+ <item name="type14">TypedArray Test!</item>
+ <item name="type15">@array/difficultyLevel</item>
+ <item name="type16">Typed Value!</item>
+ </style>
+
+ <style name="TextViewWithoutColorAndAppearance">
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <style name="TextViewWithColorButWithOutAppearance">
+ <item name="android:textColor">#ff0000ff</item>
+ </style>
+
+ <style name="TextViewWithColorAndAppearance">
+ <item name="android:textColor">#ff0000ff</item>
+ <item name="android:textAppearance">@style/TextAppearance.WithColor</item>
+ </style>
+
+ <style name="TextViewWithoutColorButWithAppearance">
+ <item name="android:textAppearance">@style/TextAppearance.WithColor</item>
+ </style>
+
+ <style name="TextAppearance" parent="android:TextAppearance">
+ </style>
+
+ <style name="TextAppearance.WithColor">
+ <item name="android:textColor">#ffff0000</item>
+ </style>
+
+ <style name="TextAppearance.All">
+ <item name="android:textColor">@drawable/black</item>
+ <item name="android:textSize">20px</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textColorHint">@drawable/red</item>
+ <item name="android:textColorLink">@drawable/blue</item>
+ <item name="android:textColorHighlight">@drawable/yellow</item>
+ </style>
+
+ <style name="TextAppearance.Colors">
+ <item name="android:textColor">@drawable/black</item>
+ <item name="android:textColorHint">@drawable/blue</item>
+ <item name="android:textColorLink">@drawable/yellow</item>
+ <item name="android:textColorHighlight">@drawable/red</item>
+ </style>
+
+ <style name="TextAppearance.NotColors">
+ <item name="android:textSize">17px</item>
+ <item name="android:typeface">sans</item>
+ <item name="android:textStyle">normal</item>
+ </style>
+
+ <style name="TextAppearance.Style">
+ <item name="android:textStyle">normal</item>
+ </style>
+
+ <style name="TestEnum1">
+ <item name="testEnum">val1</item>
+ </style>
+
+ <style name="TestEnum2">
+ <item name="testEnum">val2</item>
+ </style>
+
+ <style name="TestEnum10">
+ <item name="testEnum">val10</item>
+ </style>
+
+ <style name="TestFlag1">
+ <item name="testFlags">bit1</item>
+ </style>
+
+ <style name="TestFlag2">
+ <item name="testFlags">bit2</item>
+ </style>
+
+ <style name="TestFlag31">
+ <item name="testFlags">bit31</item>
+ </style>
+
+ <style name="TestFlag1And2">
+ <item name="testFlags">bit1|bit2</item>
+ </style>
+
+ <style name="TestFlag1And2And31">
+ <item name="testFlags">bit1|bit2|bit31</item>
+ </style>
+
+ <style name="TestEnum1.EmptyInherit" />
+
+ <style name="Theme_AlertDialog">
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <style name="TestProgressBar">
+ <item name="android:indeterminateOnly">false</item>
+ <item name="android:progressDrawable">@android:drawable/progress_horizontal</item>
+ <item name="android:indeterminateDrawable">@android:drawable/progress_horizontal</item>
+ <item name="android:minHeight">20dip</item>
+ <item name="android:maxHeight">20dip</item>
+ <item name="android:focusable">true</item>
+ </style>
+
+ <style name="Test_Theme">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:panelColorForeground">#ff000000</item>
+ <item name="android:panelColorBackground">#ffffffff</item>
+ </style>
+
+ <style name="Theme_OverrideOuter">
+ <item name="themeType">1</item>
+ </style>
+
+ <style name="Theme_OverrideInner">
+ <item name="themeType">2</item>
+ <item name="themeOverrideAttr">@style/Theme_OverrideAttr</item>
+ </style>
+
+ <style name="Theme_OverrideAttr">
+ <item name="themeType">3</item>
+ </style>
+
+ <style name="Theme_ThemedDrawableTest">
+ <item name="themeBoolean">true</item>
+ <item name="themeColor">@android:color/black</item>
+ <item name="themeFloat">1.0</item>
+ <item name="themeAngle">45.0</item>
+ <item name="themeInteger">1</item>
+ <item name="themeDimension">1px</item>
+ <item name="themeDrawable">@drawable/icon_black</item>
+ <item name="themeBitmap">@drawable/icon_black</item>
+ <item name="themeNinePatch">@drawable/ninepatch_0</item>
+ <item name="themeGravity">48</item>
+ <item name="themeTileMode">2</item>
+ <item name="themeType">0</item>
+ <item name="themeVectorDrawableFillColor">#F00F</item>
+ </style>
+
+ <attr name="colorPrimary" format="reference|color" />
+ <attr name="colorPrimaryDark" format="reference|color" />
+ <attr name="colorAccent" format="reference|color" />
+
+ <style name="Theme_MixedGradientTheme">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+
+ <!-- overwrite the default gradient type to be radial in the theme -->
+ <item name="GradientType">1</item>
+ </style>
+
+ <!-- default gradient type of linear -->
+ <attr name="GradientType" format="integer">0</attr>
+
+ <style name="WhiteBackgroundNoWindowAnimation"
+ parent="@android:style/Theme.Holo.NoActionBar.Fullscreen">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowOverscan">true</item>
+ <item name="android:fadingEdge">none</item>
+ <item name="android:windowBackground">@android:color/white</item>
+ <item name="android:windowContentTransitions">false</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
+
+</resources>
diff --git a/integration_tests/nativegraphics/src/main/res/values/themes.xml b/integration_tests/nativegraphics/src/main/res/values/themes.xml
new file mode 100644
index 000000000..50cc0e8b5
--- /dev/null
+++ b/integration_tests/nativegraphics/src/main/res/values/themes.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+ <style name="DefaultTheme" parent="@android:style/Theme.Material.NoActionBar.Fullscreen">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowDisablePreview">true</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowOverscan">true</item>
+ <item name="android:fadingEdge">none</item>
+ <item name="android:windowBackground">@android:color/white</item>
+ <item name="android:windowContentTransitions">false</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:ambientShadowAlpha">0.039</item>
+ <item name="android:spotShadowAlpha">0.19</item>
+ <item name="android:forceDarkAllowed">false</item>
+ </style>
+ <style name="AutoDarkTheme" parent="@style/DefaultTheme">
+ <item name="android:forceDarkAllowed">true</item>
+ <item name="android:isLightTheme">true</item>
+ </style>
+</resources>
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/BitmapLoadingTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/BitmapLoadingTest.java
new file mode 100644
index 000000000..5d212d245
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/BitmapLoadingTest.java
@@ -0,0 +1,37 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.graphics.Bitmap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = O)
+@RunWith(RobolectricTestRunner.class)
+public class BitmapLoadingTest {
+
+ /**
+ * There is a potential race that happens if Bitmap classes are loaded by multiple threads. The
+ * race is due to the lazy initialization of ColorSpace class members in hwui's jni/Graphics.cpp.
+ */
+ @Test
+ public void bitmapLoading_backgroundThreads_doesNotRace() throws Exception {
+ ExecutorService executorService = Executors.newFixedThreadPool(4);
+ for (int i = 0; i < 10; i++) {
+ executorService.execute(
+ () -> {
+ Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+ // createScaledBitmap triggers lazy ColorSpace member initialization in hwui
+ // Graphics.cpp.
+ Bitmap.createScaledBitmap(bitmap, 200, 200, false);
+ });
+ }
+ executorService.shutdown();
+ executorService.awaitTermination(10, SECONDS);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/BitmapUtils.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/BitmapUtils.java
new file mode 100644
index 000000000..388d005b2
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/BitmapUtils.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Color;
+import android.util.Log;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.Random;
+
+public class BitmapUtils {
+ private static final String TAG = "BitmapUtils";
+
+ private BitmapUtils() {}
+
+ private static Boolean compareBasicBitmapsInfo(Bitmap bmp1, Bitmap bmp2) {
+ if (bmp1 == bmp2) {
+ return Boolean.TRUE;
+ }
+
+ if (bmp1 == null) {
+ Log.d(TAG, "compareBitmaps() failed because bmp1 is null");
+ return Boolean.FALSE;
+ }
+
+ if (bmp2 == null) {
+ Log.d(TAG, "compareBitmaps() failed because bmp2 is null");
+ return Boolean.FALSE;
+ }
+
+ if ((bmp1.getWidth() != bmp2.getWidth()) || (bmp1.getHeight() != bmp2.getHeight())) {
+ Log.d(
+ TAG,
+ "compareBitmaps() failed because sizes don't match "
+ + "bmp1=("
+ + bmp1.getWidth()
+ + "x"
+ + bmp1.getHeight()
+ + "), "
+ + "bmp2=("
+ + bmp2.getWidth()
+ + "x"
+ + bmp2.getHeight()
+ + ")");
+ return Boolean.FALSE;
+ }
+
+ if (bmp1.getConfig() != bmp2.getConfig()) {
+ Log.d(
+ TAG,
+ "compareBitmaps() failed because configs don't match "
+ + "bmp1=("
+ + bmp1.getConfig()
+ + "), "
+ + "bmp2=("
+ + bmp2.getConfig()
+ + ")");
+ return Boolean.FALSE;
+ }
+
+ return null;
+ }
+
+ /** Compares two bitmaps by pixels. */
+ public static boolean compareBitmaps(Bitmap bmp1, Bitmap bmp2) {
+ final Boolean basicComparison = compareBasicBitmapsInfo(bmp1, bmp2);
+ if (basicComparison != null) {
+ return basicComparison.booleanValue();
+ }
+
+ for (int i = 0; i < bmp1.getWidth(); i++) {
+ for (int j = 0; j < bmp1.getHeight(); j++) {
+ if (bmp1.getPixel(i, j) != bmp2.getPixel(i, j)) {
+ Log.d(TAG, "compareBitmaps(): pixels (" + i + ", " + j + ") don't match");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Compares two bitmaps by pixels, with a buffer for mismatches.
+ *
+ * <p>For example, if {@code minimumPrecision} is 0.99, at least 99% of the pixels should match.
+ */
+ public static boolean compareBitmaps(Bitmap bmp1, Bitmap bmp2, double minimumPrecision) {
+ final Boolean basicComparison = compareBasicBitmapsInfo(bmp1, bmp2);
+ if (basicComparison != null) {
+ return basicComparison.booleanValue();
+ }
+
+ final int width = bmp1.getWidth();
+ final int height = bmp1.getHeight();
+
+ final long numberPixels = (long) width * height;
+ long numberMismatches = 0;
+
+ for (int i = 0; i < width; i++) {
+ for (int j = 0; j < height; j++) {
+ if (bmp1.getPixel(i, j) != bmp2.getPixel(i, j)) {
+ numberMismatches++;
+ if (numberMismatches <= 10) {
+ // Let's not spam logcat...
+ Log.w(TAG, "compareBitmaps(): pixels (" + i + ", " + j + ") don't match");
+ }
+ }
+ }
+ }
+ final double actualPrecision = ((double) numberPixels - numberMismatches) / numberPixels;
+ Log.v(
+ TAG,
+ "compareBitmaps(): numberPixels="
+ + numberPixels
+ + ", numberMismatches="
+ + numberMismatches
+ + ", minimumPrecision="
+ + minimumPrecision
+ + ", actualPrecision="
+ + actualPrecision);
+ return actualPrecision >= minimumPrecision;
+ }
+
+ public static Bitmap generateRandomBitmap(int width, int height) {
+ final Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Random generator = new Random();
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ bmp.setPixel(x, y, generator.nextInt(Integer.MAX_VALUE));
+ }
+ }
+ return bmp;
+ }
+
+ public static Bitmap generateWhiteBitmap(int width, int height) {
+ final Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bmp.eraseColor(Color.WHITE);
+ return bmp;
+ }
+
+ public static ByteArrayInputStream bitmapToInputStream(Bitmap bmp) {
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ bmp.compress(CompressFormat.PNG, 0 /*ignored for PNG*/, bos);
+ byte[] bitmapData = bos.toByteArray();
+ return new ByteArrayInputStream(bitmapData);
+ }
+
+ private static void logIfBitmapSolidColor(String fileName, Bitmap bitmap) {
+ int firstColor = bitmap.getPixel(0, 0);
+ for (int x = 0; x < bitmap.getWidth(); x++) {
+ for (int y = 0; y < bitmap.getHeight(); y++) {
+ if (bitmap.getPixel(x, y) != firstColor) {
+ return;
+ }
+ }
+ }
+
+ Log.w(TAG, String.format("%s entire bitmap color is %x", fileName, firstColor));
+ }
+
+ public static void saveBitmap(Bitmap bitmap, String directoryName, String fileName) {
+ new File(directoryName).mkdirs(); // create dirs if needed
+
+ Log.d(TAG, "Saving file: " + fileName + " in directory: " + directoryName);
+
+ if (bitmap == null) {
+ Log.d(TAG, "File not saved, bitmap was null");
+ return;
+ }
+
+ logIfBitmapSolidColor(fileName, bitmap);
+
+ File file = new File(directoryName, fileName);
+ try (FileOutputStream fileStream = new FileOutputStream(file)) {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 0 /* ignored for PNG */, fileStream);
+ fileStream.flush();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Compare expected to actual to see if their diff is less than mseMargin.
+ // lessThanMargin is to indicate whether we expect the diff to be
+ // "less than" or "no less than".
+ public static boolean compareBitmapsMse(
+ Bitmap expected,
+ Bitmap actual,
+ int mseMargin,
+ boolean lessThanMargin,
+ boolean isPremultiplied) {
+ final Boolean basicComparison = compareBasicBitmapsInfo(expected, actual);
+ if (basicComparison != null) {
+ return basicComparison.booleanValue();
+ }
+
+ double mse = 0;
+ int width = expected.getWidth();
+ int height = expected.getHeight();
+
+ // Bitmap.getPixels() returns colors with non-premultiplied ARGB values.
+ int[] expColors = new int[width * height];
+ expected.getPixels(expColors, 0, width, 0, 0, width, height);
+
+ int[] actualColors = new int[width * height];
+ actual.getPixels(actualColors, 0, width, 0, 0, width, height);
+
+ for (int row = 0; row < height; ++row) {
+ for (int col = 0; col < width; ++col) {
+ int idx = row * width + col;
+ mse += distance(expColors[idx], actualColors[idx], isPremultiplied);
+ }
+ }
+ mse /= width * height;
+
+ Log.i(TAG, "MSE: " + mse);
+ if (lessThanMargin) {
+ if (mse > mseMargin) {
+ Log.d(TAG, "MSE too large for normal case: " + mse);
+ return false;
+ }
+ return true;
+ } else {
+ if (mse <= mseMargin) {
+ Log.d(TAG, "MSE too small for abnormal case: " + mse);
+ return false;
+ }
+ return true;
+ }
+ }
+
+ // Same as above, but asserts compareBitmapsMse's return value.
+ public static void assertBitmapsMse(
+ Bitmap expected,
+ Bitmap actual,
+ int mseMargin,
+ boolean lessThanMargin,
+ boolean isPremultiplied) {
+ assertTrue(compareBitmapsMse(expected, actual, mseMargin, lessThanMargin, isPremultiplied));
+ }
+
+ private static int multiplyAlpha(int color, int alpha) {
+ return (color * alpha + 127) / 255;
+ }
+
+ // For the Bitmap with Alpha, multiply the Alpha values to get the effective
+ // RGB colors and then compute the color-distance.
+ private static double distance(int expect, int actual, boolean isPremultiplied) {
+ if (isPremultiplied) {
+ final int a1 = Color.alpha(actual);
+ final int a2 = Color.alpha(expect);
+ final int r = multiplyAlpha(Color.red(actual), a1) - multiplyAlpha(Color.red(expect), a2);
+ final int g = multiplyAlpha(Color.green(actual), a1) - multiplyAlpha(Color.green(expect), a2);
+ final int b = multiplyAlpha(Color.blue(actual), a1) - multiplyAlpha(Color.blue(expect), a2);
+ return r * r + g * g + b * b;
+ } else {
+ int r = Color.red(actual) - Color.red(expect);
+ int g = Color.green(actual) - Color.green(expect);
+ int b = Color.blue(actual) - Color.blue(expect);
+ return r * r + g * g + b * b;
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ColorUtils.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ColorUtils.java
new file mode 100644
index 000000000..9a116a16f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ColorUtils.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static org.junit.Assert.fail;
+
+import android.graphics.Color;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.util.function.Function;
+import java.util.function.IntUnaryOperator;
+
+public final class ColorUtils {
+ public static void verifyColor(int expected, int observed) {
+ verifyColor(expected, observed, 0);
+ }
+
+ public static void verifyColor(int expected, int observed, int tolerance) {
+ verifyColor("", expected, observed, tolerance);
+ }
+
+ /**
+ * Verify that two colors match within a per-channel tolerance.
+ *
+ * @param s String with extra information about the test with an error.
+ * @param expected Expected color.
+ * @param observed Observed color.
+ * @param tolerance Per-channel tolerance by which the color can mismatch.
+ */
+ public static void verifyColor(@NonNull String s, int expected, int observed, int tolerance) {
+ s +=
+ " expected 0x"
+ + Integer.toHexString(expected)
+ + ", observed 0x"
+ + Integer.toHexString(observed)
+ + ", tolerated channel error 0x"
+ + tolerance;
+ String red = verifyChannel("red", expected, observed, tolerance, (i) -> Color.red(i));
+ String green = verifyChannel("green", expected, observed, tolerance, (i) -> Color.green(i));
+ String blue = verifyChannel("blue", expected, observed, tolerance, (i) -> Color.blue(i));
+ String alpha = verifyChannel("alpha", expected, observed, tolerance, (i) -> Color.alpha(i));
+
+ buildErrorString(s, red, green, blue, alpha);
+ }
+
+ /**
+ * Verify that two colors match within a per-channel tolerance.
+ *
+ * @param msg String with extra information about the test with an error.
+ * @param expected Expected color.
+ * @param observed Observed color.
+ * @param tolerance Per-channel tolerance by which the color can mismatch.
+ */
+ public static void verifyColor(
+ @NonNull String msg, Color expected, Color observed, float tolerance) {
+ if (!expected.getColorSpace().equals(observed.getColorSpace())) {
+ fail(
+ "Cannot compare Colors with different color spaces! expected: "
+ + expected
+ + "\tobserved: "
+ + observed);
+ }
+ msg +=
+ " expected "
+ + expected
+ + ", observed "
+ + observed
+ + ", tolerated channel error "
+ + tolerance;
+ String red = verifyChannel("red", expected, observed, tolerance, (c) -> c.red());
+ String green = verifyChannel("green", expected, observed, tolerance, (c) -> c.green());
+ String blue = verifyChannel("blue", expected, observed, tolerance, (c) -> c.blue());
+ String alpha = verifyChannel("alpha", expected, observed, tolerance, (c) -> c.alpha());
+
+ buildErrorString(msg, red, green, blue, alpha);
+ }
+
+ private static void buildErrorString(
+ @NonNull String s,
+ @Nullable String red,
+ @Nullable String green,
+ @Nullable String blue,
+ @Nullable String alpha) {
+ String err = null;
+ for (String channel : new String[] {red, green, blue, alpha}) {
+ if (channel == null) {
+ continue;
+ }
+ if (err == null) {
+ err = s;
+ }
+ err += "\n\t\t" + channel;
+ }
+ if (err != null) {
+ fail(err);
+ }
+ }
+
+ private static String verifyChannel(
+ String channelName, int expected, int observed, int tolerance, IntUnaryOperator f) {
+ int e = f.applyAsInt(expected);
+ int o = f.applyAsInt(observed);
+ if (Math.abs(e - o) <= tolerance) {
+ return null;
+ }
+ return "Channel "
+ + channelName
+ + " mismatch: expected<0x"
+ + Integer.toHexString(e)
+ + ">, observed: <0x"
+ + Integer.toHexString(o)
+ + ">";
+ }
+
+ private static String verifyChannel(
+ String channelName,
+ Color expected,
+ Color observed,
+ float tolerance,
+ Function<Color, Float> f) {
+ float e = f.apply(expected);
+ float o = f.apply(observed);
+ float diff = Math.abs(e - o);
+ if (diff <= tolerance) {
+ return null;
+ }
+ return "Channel "
+ + channelName
+ + " mismatch: expected<"
+ + e
+ + ">, observed: <"
+ + o
+ + ">, difference: <"
+ + diff
+ + ">";
+ }
+
+ private ColorUtils() {}
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/DrawableTestUtils.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/DrawableTestUtils.java
new file mode 100644
index 000000000..1167a6cbc
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/DrawableTestUtils.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static java.lang.Math.max;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import androidx.annotation.IntegerRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.base.MoreObjects;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import org.junit.Assert;
+import org.robolectric.RuntimeEnvironment;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** The useful methods for graphics.drawable test. */
+public final class DrawableTestUtils {
+ private static final String LOGTAG = "DrawableTestUtils";
+ // A small value is actually making sure that the values are matching
+ // exactly with the golden image.
+ // We can increase the threshold if the Skia is drawing with some variance
+ // on different devices. So far, the tests show they are matching correctly.
+ static final float PIXEL_ERROR_THRESHOLD = 0.03f;
+ static final float PIXEL_ERROR_COUNT_THRESHOLD = 0.005f;
+ static final int PIXEL_ERROR_TOLERANCE = 3;
+
+ public static void skipCurrentTag(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {}
+ }
+
+ /**
+ * Retrieve an AttributeSet from a XML.
+ *
+ * @param parser the XmlPullParser to use for the xml parsing.
+ * @param searchedNodeName the name of the target node.
+ * @return the AttributeSet retrieved from specified node.
+ */
+ public static AttributeSet getAttributeSet(XmlResourceParser parser, String searchedNodeName)
+ throws XmlPullParserException, IOException {
+ AttributeSet attrs = null;
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {}
+ String nodeName = parser.getName();
+ if (!"alias".equals(nodeName)) {
+ throw new RuntimeException();
+ }
+ int outerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+ nodeName = parser.getName();
+ if (searchedNodeName.equals(nodeName)) {
+ outerDepth = parser.getDepth();
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+ nodeName = parser.getName();
+ attrs = Xml.asAttributeSet(parser);
+ break;
+ }
+ break;
+ } else {
+ skipCurrentTag(parser);
+ }
+ }
+ return attrs;
+ }
+
+ public static XmlResourceParser getResourceParser(Resources res, int resId)
+ throws XmlPullParserException, IOException {
+ final XmlResourceParser parser = res.getXml(resId);
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop
+ }
+ return parser;
+ }
+
+ public static void setResourcesDensity(Resources res, int densityDpi) {
+ final Configuration config = new Configuration();
+ config.setTo(res.getConfiguration());
+ config.densityDpi = densityDpi;
+ res.updateConfiguration(config, null);
+ }
+
+ /**
+ * Implements scaling as used by the Bitmap class. Resulting values are rounded up (as distinct
+ * from resource scaling, which truncates or rounds to the nearest pixel).
+ *
+ * @param size the pixel size to scale
+ * @param sdensity the source density that corresponds to the size
+ * @param tdensity the target density
+ * @return the pixel size scaled for the target density
+ */
+ public static int scaleBitmapFromDensity(int size, int sdensity, int tdensity) {
+ if (sdensity == 0 || tdensity == 0 || sdensity == tdensity) {
+ return size;
+ }
+
+ // Scale by tdensity / sdensity, rounding up.
+ return ((size * tdensity) + (sdensity >> 1)) / sdensity;
+ }
+
+ /**
+ * Asserts that two images are similar within the given thresholds.
+ *
+ * @param message Error message
+ * @param expected Expected bitmap
+ * @param actual Actual bitmap
+ * @param pixelThreshold The total difference threshold for a single pixel
+ * @param pixelCountThreshold The total different pixel count threshold
+ * @param pixelDiffTolerance The pixel value difference tolerance
+ */
+ public static void compareImages(
+ String message,
+ Bitmap expected,
+ Bitmap actual,
+ float pixelThreshold,
+ float pixelCountThreshold,
+ int pixelDiffTolerance) {
+ int idealWidth = expected.getWidth();
+ int idealHeight = expected.getHeight();
+
+ assertEquals(actual.getWidth(), idealWidth);
+ assertEquals(actual.getHeight(), idealHeight);
+
+ int totalDiffPixelCount = 0;
+ float totalPixelCount = idealWidth * idealHeight;
+
+ for (int x = 0; x < idealWidth; x++) {
+ for (int y = 0; y < idealHeight; y++) {
+ int idealColor = expected.getPixel(x, y);
+ int givenColor = actual.getPixel(x, y);
+ if (idealColor == givenColor) {
+ continue;
+ }
+ if (Color.alpha(idealColor) + Color.alpha(givenColor) == 0) {
+ continue;
+ }
+
+ float idealAlpha = Color.alpha(idealColor) / 255.0f;
+ float givenAlpha = Color.alpha(givenColor) / 255.0f;
+
+ // compare premultiplied color values
+ float totalError = 0;
+ totalError +=
+ Math.abs((idealAlpha * Color.red(idealColor)) - (givenAlpha * Color.red(givenColor)));
+ totalError +=
+ Math.abs(
+ (idealAlpha * Color.green(idealColor)) - (givenAlpha * Color.green(givenColor)));
+ totalError +=
+ Math.abs((idealAlpha * Color.blue(idealColor)) - (givenAlpha * Color.blue(givenColor)));
+ totalError += Math.abs(Color.alpha(idealColor) - Color.alpha(givenColor));
+
+ if ((totalError / 1024.0f) >= pixelThreshold) {
+ Assert.fail(
+ (message
+ + ": totalError is "
+ + totalError
+ + " | given: "
+ + givenColor
+ + " ideal: "
+ + idealColor));
+ }
+
+ if (totalError > pixelDiffTolerance) {
+ totalDiffPixelCount++;
+ }
+ }
+ }
+
+ // TEST_UNDECLARED_OUTPUTS_DIR is better in a Bazel environment because the files show up
+ // in test artifacts.
+ String outputDir =
+ MoreObjects.firstNonNull(
+ System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"), System.getProperty("java.io.tmpdir"));
+ try {
+ File f = new File(outputDir, "expected_" + RuntimeEnvironment.getApiLevel() + ".png");
+ f.createNewFile();
+ f.deleteOnExit();
+ expected.compress(CompressFormat.PNG, 100, new FileOutputStream(f));
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+
+ try {
+ File f = new File(outputDir, "actual.png");
+ f.createNewFile();
+ f.deleteOnExit();
+ actual.compress(CompressFormat.PNG, 100, new FileOutputStream(f));
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+
+ if ((totalDiffPixelCount / totalPixelCount) >= pixelCountThreshold) {
+ Assert.fail((message + ": totalDiffPixelCount is " + totalDiffPixelCount));
+ }
+ }
+
+ /** Returns the {@link Color} at the specified location in the {@link Drawable}. */
+ public static int getPixel(Drawable d, int x, int y) {
+ final int w = max(d.getIntrinsicWidth(), x + 1);
+ final int h = max(d.getIntrinsicHeight(), y + 1);
+ final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ final Canvas c = new Canvas(b);
+ d.setBounds(0, 0, w, h);
+ d.draw(c);
+
+ final int pixel = b.getPixel(x, y);
+ b.recycle();
+ return pixel;
+ }
+
+ /**
+ * Save a bitmap for debugging or golden image (re)generation purpose. The file name will be
+ * referred from the resource id, plus optionally {@code extras}, and "_golden"
+ */
+ static void saveAutoNamedVectorDrawableIntoPNG(
+ @NonNull Context context,
+ @NonNull Bitmap bitmap,
+ @IntegerRes int resId,
+ @Nullable String extras)
+ throws IOException {
+ String originalFilePath = context.getResources().getString(resId);
+ File originalFile = new File(originalFilePath);
+ String fileFullName = originalFile.getName();
+ String fileTitle = fileFullName.substring(0, fileFullName.lastIndexOf("."));
+ String outputFolder = context.getExternalFilesDir(null).getAbsolutePath();
+ if (extras != null) {
+ fileTitle += "_" + extras;
+ }
+ saveVectorDrawableIntoPNG(bitmap, outputFolder, fileTitle);
+ }
+
+ /** Save a {@code bitmap} to the {@code fileFullName} plus "_golden". */
+ static void saveVectorDrawableIntoPNG(
+ @NonNull Bitmap bitmap, @NonNull String outputFolder, @NonNull String fileFullName)
+ throws IOException {
+ // Save the image to the disk.
+ FileOutputStream out = null;
+ try {
+ File folder = new File(outputFolder);
+ if (!folder.exists()) {
+ folder.mkdir();
+ }
+ String outputFilename = outputFolder + "/" + fileFullName + "_golden";
+ outputFilename += ".png";
+ File outputFile = new File(outputFilename);
+ if (!outputFile.exists()) {
+ outputFile.createNewFile();
+ }
+
+ out = new FileOutputStream(outputFile, false);
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+ Log.v(LOGTAG, "Write test No." + outputFilename + " to file successfully.");
+ } catch (Exception e) {
+ // Unused
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+
+ private DrawableTestUtils() {}
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/FontLoadingTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/FontLoadingTest.java
new file mode 100644
index 000000000..3c5e4ca31
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/FontLoadingTest.java
@@ -0,0 +1,27 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import androidx.core.content.res.ResourcesCompat;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = O)
+@RunWith(RobolectricTestRunner.class)
+public class FontLoadingTest {
+
+ /**
+ * There is a potential static initializer cycle that can happen when creating fonts triggers
+ * loading RNR. Because statically initializing Typeface is the last step of loading RNR, and the
+ * static initializer of Typeface causes a lot of Font objects to be created, there has to be
+ * special logic when fonts are loaded to avoid statically initializing Typeface. This is captured
+ * in a test here.
+ */
+ @Test
+ public void loadFont_doesNotCauseStaticInitializerCycle() {
+ ResourcesCompat.getFont(RuntimeEnvironment.getApplication(), R.font.multiweight_family);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/HardwareAcceleratedActivityRenderTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/HardwareAcceleratedActivityRenderTest.java
new file mode 100644
index 000000000..3828aa087
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/HardwareAcceleratedActivityRenderTest.java
@@ -0,0 +1,29 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.S;
+
+import android.app.Activity;
+import android.view.WindowManager;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = S)
+public class HardwareAcceleratedActivityRenderTest {
+ @Test
+ public void setupHardwareAcceleratedActivity() {
+ // This will exercise much of the HardwareRenderer / RenderNode / RecordingCanvas native code.
+ ActivityController<Activity> controller = Robolectric.buildActivity(Activity.class);
+ controller
+ .get()
+ .getWindow()
+ .setFlags(
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
+ controller.setup();
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/MediaStoreTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/MediaStoreTest.java
new file mode 100644
index 000000000..f6f2bd5bf
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/MediaStoreTest.java
@@ -0,0 +1,28 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.provider.MediaStore;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class MediaStoreTest {
+
+ @Test
+ public void getBitmap_legacyAPI_alwaysReturnsABitmap() throws IOException {
+ Uri screenshotUri = new Uri.Builder().scheme("content").appendPath("invalid_file").build();
+ Bitmap bitmap =
+ MediaStore.Images.Media.getBitmap(
+ RuntimeEnvironment.getApplication().getContentResolver(), screenshotUri);
+ assertThat(bitmap).isNotNull();
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ScrollViewTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ScrollViewTest.java
new file mode 100644
index 000000000..985d3d5d1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ScrollViewTest.java
@@ -0,0 +1,36 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.widget.ScrollView;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public final class ScrollViewTest {
+
+ /**
+ * Checks that even if {@code robolectric.useRealScrolling} is set to false, real scrolling code
+ * is used when RNG is enabled.
+ */
+ @Test
+ public void smoothScrollTo_usesRealCode_whenRNGEnabled() {
+ try {
+ System.setProperty("robolectric.useRealScrolling", "false");
+ Activity activity = Robolectric.setupActivity(Activity.class);
+ ScrollView scrollView = new ScrollView(activity);
+ scrollView.smoothScrollTo(100, 100);
+ // Because the scroll view has no children, it should not get scrolled.
+ assertThat(scrollView.getScrollX()).isEqualTo(0);
+ assertThat(scrollView.getScrollY()).isEqualTo(0);
+ } finally {
+ System.clearProperty("robolectric.useRealScrolling");
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAllocationRegistryTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAllocationRegistryTest.java
new file mode 100644
index 000000000..14f74de93
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAllocationRegistryTest.java
@@ -0,0 +1,24 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 30)
+public final class ShadowNativeAllocationRegistryTest {
+
+ @Test
+ public void nativeAllocationRegistryStressTest() {
+ for (int i = 0; i < 10_000; i++) {
+ Bitmap bitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.BLUE);
+ if (i % 100 == 0) {
+ System.gc();
+ }
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedImageDrawableTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedImageDrawableTest.java
new file mode 100644
index 000000000..03db7e98b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedImageDrawableTest.java
@@ -0,0 +1,36 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.P;
+import static com.google.common.base.StandardSystemProperty.OS_NAME;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import android.content.res.Resources;
+import android.graphics.drawable.AnimatedImageDrawable;
+import java.util.Locale;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = P)
+public class ShadowNativeAnimatedImageDrawableTest {
+ private Resources resources;
+
+ @Before
+ public void setup() {
+ // The native code behind AnimatedImageDrawable makes use of Linux-specific APIs (epoll),
+ // so it doesn't work on Mac at the moment.
+ assume().that(OS_NAME.value().toLowerCase(Locale.US)).doesNotContain("mac");
+ resources = RuntimeEnvironment.getApplication().getResources();
+ }
+
+ @Test
+ public void testInflate() throws Exception {
+ AnimatedImageDrawable aid = (AnimatedImageDrawable) resources.getDrawable(R.drawable.animated);
+ assertThat(aid).isNotNull();
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java
new file mode 100644
index 000000000..77f548373
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeAnimatedVectorDrawableTest.java
@@ -0,0 +1,74 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static junit.framework.Assert.assertEquals;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Xml;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowDrawable;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowNativeAnimatedVectorDrawableTest {
+ private static final int IMAGE_WIDTH = 64;
+ private static final int IMAGE_HEIGHT = 64;
+
+ private static final int RES_ID = R.drawable.animation_vector_drawable_grouping_1;
+
+ private Resources resources;
+
+ @Before
+ public void setup() {
+ resources = RuntimeEnvironment.getApplication().getResources();
+ }
+
+ @Test
+ public void testInflate() throws Exception {
+ // Setup AnimatedVectorDrawable from xml file
+ XmlPullParser parser = resources.getXml(RES_ID);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+ Bitmap bitmap = Bitmap.createBitmap(IMAGE_WIDTH, IMAGE_HEIGHT, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ AnimatedVectorDrawable drawable = new AnimatedVectorDrawable();
+ drawable.inflate(resources, parser, attrs);
+ drawable.setBounds(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
+ bitmap.eraseColor(0);
+ drawable.draw(canvas);
+ int sunColor = bitmap.getPixel(IMAGE_WIDTH / 2, IMAGE_HEIGHT / 2);
+ int earthColor = bitmap.getPixel(IMAGE_WIDTH * 3 / 4 + 2, IMAGE_HEIGHT / 2);
+ assertEquals(0xFFFF8000, sunColor);
+ assertEquals(0xFF5656EA, earthColor);
+ }
+
+ @Test
+ public void legacyShadowDrawableAPI() {
+ Drawable drawable = resources.getDrawable(RES_ID);
+ ShadowDrawable shadowDrawable = Shadow.extract(drawable);
+ assertEquals(
+ R.drawable.animation_vector_drawable_grouping_1, shadowDrawable.getCreatedFromResId());
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapDrawableTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapDrawableTest.java
new file mode 100644
index 000000000..96a5c98e3
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapDrawableTest.java
@@ -0,0 +1,50 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowDrawable;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowNativeBitmapDrawableTest {
+ private static final int EXPECTED_COLOR = 0xff0000fe;
+
+ @Test
+ @Config(qualifiers = "hdpi")
+ public void bitmapDrawable_highDensity() {
+ Drawable drawable = RuntimeEnvironment.getApplication().getDrawable(R.drawable.icon_blue);
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ Bitmap output =
+ Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+ drawable.draw(canvas);
+ // top left and bottom right colors should match
+ assertThat(output.getPixel(0, 0)).isEqualTo(EXPECTED_COLOR);
+ assertThat(output.getPixel(drawable.getIntrinsicWidth() - 1, drawable.getIntrinsicHeight() - 1))
+ .isEqualTo(EXPECTED_COLOR);
+ }
+
+ @Test
+ public void getCreatedFromResId() {
+ Drawable drawable = RuntimeEnvironment.getApplication().getDrawable(R.drawable.icon_blue);
+ assertThat(((ShadowDrawable) Shadow.extract(drawable)).getCreatedFromResId())
+ .isEqualTo(R.drawable.icon_blue);
+ }
+
+ @Test
+ public void legacy_createFromResourceId() {
+ Drawable drawable = ShadowDrawable.createFromResourceId(100);
+ assertThat(((ShadowDrawable) Shadow.extract(drawable)).getCreatedFromResId()).isEqualTo(100);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapFactoryTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapFactoryTest.java
new file mode 100644
index 000000000..31961b6f4
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapFactoryTest.java
@@ -0,0 +1,561 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.graphics.Bitmap.Config.ARGB_8888;
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static com.google.common.truth.Truth.assertThat;
+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.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.ParcelFileDescriptor;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowNativeBitmapFactoryTest {
+ // height and width of start.jpg
+ private static final int START_HEIGHT = 31;
+ private static final int START_WIDTH = 31;
+
+ static class TestImage {
+ TestImage(int id, int width, int height) {
+ this.id = id;
+ this.width = width;
+ this.height = height;
+ }
+
+ public final int id;
+ public final int width;
+ public final int height;
+ }
+
+ private TestImage[] testImages() {
+ return new TestImage[] {
+ new TestImage(R.drawable.baseline_jpeg, 1280, 960),
+ new TestImage(R.drawable.png_test, 640, 480),
+ new TestImage(R.drawable.gif_test, 320, 240),
+ new TestImage(R.drawable.bmp_test, 320, 240),
+ new TestImage(R.drawable.webp_test, 640, 480),
+ };
+ }
+
+ private Resources res;
+ // opt for non-null
+ private BitmapFactory.Options opt1;
+ // opt for null
+ private BitmapFactory.Options opt2;
+ private int defaultDensity;
+ private int targetDensity;
+
+ @Before
+ public void setup() {
+ res = RuntimeEnvironment.getApplication().getResources();
+ defaultDensity = DisplayMetrics.DENSITY_DEFAULT;
+ targetDensity = res.getDisplayMetrics().densityDpi;
+
+ opt1 = new BitmapFactory.Options();
+ opt1.inScaled = false;
+ opt2 = new BitmapFactory.Options();
+ opt2.inScaled = false;
+ opt2.inJustDecodeBounds = true;
+ }
+
+ @Test
+ public void testDecodeResource1() {
+ Bitmap b = BitmapFactory.decodeResource(res, R.drawable.start, opt1);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(START_HEIGHT, b.getHeight());
+ assertEquals(START_WIDTH, b.getWidth());
+ // Test if no bitmap
+ assertNull(BitmapFactory.decodeResource(res, R.drawable.start, opt2));
+ }
+
+ @Test
+ public void testDecodeResource2() {
+ Bitmap b = BitmapFactory.decodeResource(res, R.drawable.start);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(START_HEIGHT * targetDensity / ((double) defaultDensity), b.getHeight(), 1.1);
+ assertEquals(START_WIDTH * targetDensity / ((double) defaultDensity), b.getWidth(), 1.1);
+ }
+
+ @Test
+ public void testDecodeResourceStream() {
+ InputStream is = obtainInputStream();
+ Rect r = new Rect(1, 1, 1, 1);
+ TypedValue value = new TypedValue();
+ Bitmap b = BitmapFactory.decodeResourceStream(res, value, is, r, opt1);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(START_HEIGHT, b.getHeight());
+ assertEquals(START_WIDTH, b.getWidth());
+ }
+
+ @Test
+ public void testDecodeByteArray1() {
+ byte[] array = obtainArray();
+ Bitmap b = BitmapFactory.decodeByteArray(array, 0, array.length, opt1);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(START_HEIGHT, b.getHeight());
+ assertEquals(START_WIDTH, b.getWidth());
+ // Test if no bitmap
+ assertNull(BitmapFactory.decodeByteArray(array, 0, array.length, opt2));
+ }
+
+ @Test
+ public void testDecodeByteArray2() {
+ byte[] array = obtainArray();
+ Bitmap b = BitmapFactory.decodeByteArray(array, 0, array.length);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(START_HEIGHT, b.getHeight());
+ assertEquals(START_WIDTH, b.getWidth());
+ }
+
+ @Test
+ public void testDecodeStream1() {
+ InputStream is = obtainInputStream();
+ Rect r = new Rect(1, 1, 1, 1);
+ Bitmap b = BitmapFactory.decodeStream(is, r, opt1);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(START_HEIGHT, b.getHeight());
+ assertEquals(START_WIDTH, b.getWidth());
+ // Test if no bitmap
+ assertNull(BitmapFactory.decodeStream(is, r, opt2));
+ }
+
+ @Test
+ public void testDecodeStream2() {
+ InputStream is = obtainInputStream();
+ Bitmap b = BitmapFactory.decodeStream(is);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(START_HEIGHT, b.getHeight());
+ assertEquals(START_WIDTH, b.getWidth());
+ }
+
+ @Test
+ public void testDecodeStream3() {
+ for (TestImage testImage : testImages()) {
+ InputStream is = obtainInputStream(testImage.id);
+ Bitmap b = BitmapFactory.decodeStream(is);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(testImage.width, b.getWidth());
+ assertEquals(testImage.height, b.getHeight());
+ }
+ }
+
+ @Test
+ public void testDecodeStream5() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = ARGB_8888;
+
+ // Decode the PNG & WebP (google_logo) images. The WebP image has
+ // been encoded from PNG image.
+ InputStream iStreamPng = obtainInputStream(R.drawable.google_logo_1);
+ Bitmap bPng = BitmapFactory.decodeStream(iStreamPng, null, options);
+ assertNotNull(bPng);
+ assertEquals(ARGB_8888, bPng.getConfig());
+ assertTrue(bPng.isPremultiplied());
+ assertTrue(bPng.hasAlpha());
+
+ // Decode the corresponding WebP (transparent) image (google_logo_2.webp).
+ InputStream iStreamWebP1 = obtainInputStream(R.drawable.google_logo_2);
+ Bitmap bWebP1 = BitmapFactory.decodeStream(iStreamWebP1, null, options);
+ assertNotNull(bWebP1);
+ assertEquals(ARGB_8888, bWebP1.getConfig());
+ assertTrue(bWebP1.isPremultiplied());
+ assertTrue(bWebP1.hasAlpha());
+
+ // Compress the PNG image to WebP format (Quality=90) and decode it back.
+ // This will test end-to-end WebP encoding and decoding.
+ ByteArrayOutputStream oStreamWebp = new ByteArrayOutputStream();
+ assertTrue(bPng.compress(CompressFormat.WEBP, 90, oStreamWebp));
+ InputStream iStreamWebp2 = new ByteArrayInputStream(oStreamWebp.toByteArray());
+ Bitmap bWebP2 = BitmapFactory.decodeStream(iStreamWebp2, null, options);
+ assertNotNull(bWebP2);
+ assertEquals(ARGB_8888, bWebP2.getConfig());
+ assertTrue(bWebP2.isPremultiplied());
+ assertTrue(bWebP2.hasAlpha());
+ }
+
+ @Test
+ public void testDecodeReuseBasic() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ options.inSampleSize = 0; // treated as 1
+ options.inScaled = false;
+ Bitmap start = BitmapFactory.decodeResource(res, R.drawable.start, options);
+ int originalSize = start.getByteCount();
+ assertEquals(originalSize, start.getAllocationByteCount());
+
+ options.inBitmap = start;
+ options.inMutable = false; // will be overridden by non-null inBitmap
+ options.inSampleSize = -42; // treated as 1
+ Bitmap pass = BitmapFactory.decodeResource(res, R.drawable.pass, options);
+
+ assertEquals(originalSize, pass.getByteCount());
+ assertEquals(originalSize, pass.getAllocationByteCount());
+ assertSame(start, pass);
+ assertTrue(pass.isMutable());
+ }
+
+ @Test
+ public void testDecodeReuseAttempt() {
+ // BitmapFactory "silently" ignores an immutable inBitmap. (It does print a log message.)
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = false;
+
+ Bitmap start = BitmapFactory.decodeResource(res, R.drawable.start, options);
+ assertFalse(start.isMutable());
+
+ options.inBitmap = start;
+ Bitmap pass = BitmapFactory.decodeResource(res, R.drawable.pass, options);
+ assertNotNull(pass);
+ assertNotEquals(start, pass);
+ }
+
+ @Config(minSdk = Q)
+ @Test
+ public void testDecodeReuseRecycled() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+
+ Bitmap start = BitmapFactory.decodeResource(res, R.drawable.start, options);
+ assertNotNull(start);
+ start.recycle();
+
+ options.inBitmap = start;
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ BitmapFactory.decodeResource(res, R.drawable.pass, options);
+ });
+ }
+
+ /** Create bitmap sized to load unscaled resources: start, pass, and alpha */
+ private Bitmap createBitmapForReuse(int pixelCount) {
+ Bitmap bitmap = Bitmap.createBitmap(pixelCount, 1, ARGB_8888);
+ bitmap.eraseColor(Color.BLACK);
+ bitmap.setHasAlpha(false);
+ return bitmap;
+ }
+
+ /** Decode resource with ResId into reuse bitmap without scaling, verifying expected hasAlpha */
+ private void decodeResourceWithReuse(Bitmap reuse, int resId, boolean hasAlpha) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ options.inSampleSize = 1;
+ options.inScaled = false;
+ options.inBitmap = reuse;
+ Bitmap output = BitmapFactory.decodeResource(res, resId, options);
+ assertSame(reuse, output);
+ assertEquals(output.hasAlpha(), hasAlpha);
+ }
+
+ @Test
+ public void testDecodeReuseHasAlpha() {
+ final int bitmapSize = 31; // size in pixels of start, pass, and alpha resources
+ final int pixelCount = bitmapSize * bitmapSize;
+
+ // Test reuse, hasAlpha false and true
+ Bitmap bitmap = createBitmapForReuse(pixelCount);
+ decodeResourceWithReuse(bitmap, R.drawable.start, false);
+ decodeResourceWithReuse(bitmap, R.drawable.alpha, true);
+
+ // Test pre-reconfigure, hasAlpha false and true
+ bitmap = createBitmapForReuse(pixelCount);
+ bitmap.reconfigure(bitmapSize, bitmapSize, ARGB_8888);
+ bitmap.setHasAlpha(true);
+ decodeResourceWithReuse(bitmap, R.drawable.start, false);
+
+ bitmap = createBitmapForReuse(pixelCount);
+ bitmap.reconfigure(bitmapSize, bitmapSize, ARGB_8888);
+ decodeResourceWithReuse(bitmap, R.drawable.alpha, true);
+ }
+
+ @Test
+ public void testDecodeReuseFormats() {
+ for (TestImage testImage : testImages()) {
+ // reuse should support all image formats
+ Bitmap reuseBuffer = Bitmap.createBitmap(1000000, 1, Bitmap.Config.ALPHA_8);
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inBitmap = reuseBuffer;
+ options.inSampleSize = 4;
+ options.inScaled = false;
+ Bitmap decoded = BitmapFactory.decodeResource(res, testImage.id, options);
+ assertSame(reuseBuffer, decoded);
+ }
+ }
+
+ @Test
+ public void testDecodeReuseFailure() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ options.inScaled = false;
+ options.inSampleSize = 4;
+ Bitmap reduced = BitmapFactory.decodeResource(res, R.drawable.robot, options);
+
+ options.inBitmap = reduced;
+ options.inSampleSize = 1;
+ assertThrows(
+ "should throw exception due to lack of space",
+ IllegalArgumentException.class,
+ () -> BitmapFactory.decodeResource(res, R.drawable.robot, options));
+ }
+
+ @Test
+ public void testDecodeReuseScaling() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ options.inScaled = false;
+ Bitmap original = BitmapFactory.decodeResource(res, R.drawable.robot, options);
+ int originalSize = original.getByteCount();
+ assertEquals(originalSize, original.getAllocationByteCount());
+
+ options.inBitmap = original;
+ options.inSampleSize = 4;
+ Bitmap reduced = BitmapFactory.decodeResource(res, R.drawable.robot, options);
+
+ assertSame(original, reduced);
+ assertEquals(originalSize, reduced.getAllocationByteCount());
+ assertEquals(originalSize, reduced.getByteCount() * 16);
+ }
+
+ @Test
+ public void testDecodeReuseDoubleScaling() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ options.inScaled = false;
+ options.inSampleSize = 1;
+ Bitmap original = BitmapFactory.decodeResource(res, R.drawable.robot, options);
+ int originalSize = original.getByteCount();
+
+ // Verify that inSampleSize and density scaling both work with reuse concurrently
+ options.inBitmap = original;
+ options.inScaled = true;
+ options.inSampleSize = 2;
+ options.inDensity = 2;
+ options.inTargetDensity = 4;
+ Bitmap doubleScaled = BitmapFactory.decodeResource(res, R.drawable.robot, options);
+
+ assertSame(original, doubleScaled);
+ assertEquals(4, doubleScaled.getDensity());
+ assertEquals(originalSize, doubleScaled.getByteCount());
+ }
+
+ @Test
+ public void testDecodeReuseEquivalentScaling() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ options.inScaled = true;
+ options.inDensity = 4;
+ options.inTargetDensity = 2;
+ Bitmap densityReduced = BitmapFactory.decodeResource(res, R.drawable.robot, options);
+ assertEquals(2, densityReduced.getDensity());
+ options.inBitmap = densityReduced;
+ options.inDensity = 0;
+ options.inScaled = false;
+ options.inSampleSize = 2;
+ Bitmap scaleReduced = BitmapFactory.decodeResource(res, R.drawable.robot, options);
+ // verify that density isn't incorrectly carried over during bitmap reuse
+ assertFalse(densityReduced.getDensity() == 2);
+ assertFalse(densityReduced.getDensity() == 0);
+ assertSame(densityReduced, scaleReduced);
+ }
+
+ @Test
+ public void testDecodePremultipliedDefault() {
+ Bitmap simplePremul = BitmapFactory.decodeResource(res, R.drawable.premul_data);
+ assertTrue(simplePremul.isPremultiplied());
+ }
+
+ @Test
+ public void testDecodeInPurgeableAllocationCount() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 1;
+ options.inJustDecodeBounds = false;
+ options.inPurgeable = true;
+ options.inInputShareable = false;
+ byte[] array = obtainArray();
+ Bitmap purgeableBitmap = BitmapFactory.decodeByteArray(array, 0, array.length, options);
+ assertFalse(purgeableBitmap.getAllocationByteCount() == 0);
+ }
+
+ private int defaultCreationDensity;
+
+ private void verifyScaled(Bitmap b) {
+ assertEquals(START_WIDTH * 2, b.getWidth());
+ assertEquals(2, b.getDensity());
+ }
+
+ private void verifyUnscaled(Bitmap b) {
+ assertEquals(START_WIDTH, b.getWidth());
+ assertEquals(b.getDensity(), defaultCreationDensity);
+ }
+
+ @Test
+ public void testDecodeScaling() {
+ BitmapFactory.Options defaultOpt = new BitmapFactory.Options();
+
+ BitmapFactory.Options unscaledOpt = new BitmapFactory.Options();
+ unscaledOpt.inScaled = false;
+
+ BitmapFactory.Options scaledOpt = new BitmapFactory.Options();
+ scaledOpt.inScaled = true;
+ scaledOpt.inDensity = 1;
+ scaledOpt.inTargetDensity = 2;
+
+ defaultCreationDensity = Bitmap.createBitmap(1, 1, ARGB_8888).getDensity();
+
+ byte[] bytes = obtainArray();
+
+ verifyUnscaled(BitmapFactory.decodeByteArray(bytes, 0, bytes.length));
+ verifyUnscaled(BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null));
+ verifyUnscaled(BitmapFactory.decodeByteArray(bytes, 0, bytes.length, unscaledOpt));
+ verifyUnscaled(BitmapFactory.decodeByteArray(bytes, 0, bytes.length, defaultOpt));
+
+ verifyUnscaled(BitmapFactory.decodeStream(obtainInputStream()));
+ verifyUnscaled(BitmapFactory.decodeStream(obtainInputStream(), null, null));
+ verifyUnscaled(BitmapFactory.decodeStream(obtainInputStream(), null, unscaledOpt));
+ verifyUnscaled(BitmapFactory.decodeStream(obtainInputStream(), null, defaultOpt));
+
+ // scaling should only occur if Options are passed with inScaled=true
+ verifyScaled(BitmapFactory.decodeByteArray(bytes, 0, bytes.length, scaledOpt));
+ verifyScaled(BitmapFactory.decodeStream(obtainInputStream(), null, scaledOpt));
+ }
+
+ @Test
+ public void testDecodeFileDescriptor1() throws IOException {
+ ParcelFileDescriptor pfd = obtainParcelDescriptor(obtainPath());
+ FileDescriptor input = pfd.getFileDescriptor();
+ Rect r = new Rect(1, 1, 1, 1);
+ Bitmap b = BitmapFactory.decodeFileDescriptor(input, r, opt1);
+ assertNotNull(b);
+ // Test the bitmap size
+ assertEquals(START_HEIGHT, b.getHeight());
+ assertEquals(START_WIDTH, b.getWidth());
+ // Test if no bitmap
+ assertNull(BitmapFactory.decodeFileDescriptor(input, r, opt2));
+ }
+
+ private byte[] obtainArray() {
+ ByteArrayOutputStream stm = new ByteArrayOutputStream();
+ Options opt = new BitmapFactory.Options();
+ opt.inScaled = false;
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.start, opt);
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 0, stm);
+ return stm.toByteArray();
+ }
+
+ private static InputStream obtainInputStream() {
+ return RuntimeEnvironment.getApplication().getResources().openRawResource(R.drawable.start);
+ }
+
+ private static InputStream obtainInputStream(int resId) {
+ return RuntimeEnvironment.getApplication().getResources().openRawResource(resId);
+ }
+
+ private static ParcelFileDescriptor obtainParcelDescriptor(String path) throws IOException {
+ File file = new File(path);
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+
+ private static String obtainPath() throws IOException {
+ return obtainPath(R.drawable.start, 0);
+ }
+
+ static String obtainPath(int resId, long offset) throws IOException {
+ return obtainFile(resId, offset).getPath();
+ }
+
+ /**
+ * Create and return a new file.
+ *
+ * @param resId Original file. It will be copied into the new file.
+ * @param offset Number of zeroes to write to the new file before the copied file. This allows
+ * testing decodeFileDescriptor with an offset. Must be less than or equal to 1024
+ */
+ static File obtainFile(int resId, long offset) throws IOException {
+ assertTrue(offset >= 0);
+ File dir = RuntimeEnvironment.getApplication().getFilesDir();
+ dir.mkdirs();
+
+ String name =
+ RuntimeEnvironment.getApplication().getResources().getResourceEntryName(resId).toString();
+ if (offset > 0) {
+ name = name + "_" + String.valueOf(offset);
+ }
+
+ File file = new File(dir, name);
+ if (file.exists()) {
+ return file;
+ }
+
+ file.createNewFile();
+
+ InputStream is = obtainInputStream(resId);
+
+ FileOutputStream fOutput = new FileOutputStream(file);
+ byte[] dataBuffer = new byte[1024];
+ // Write a bunch of zeroes before the image.
+ assertThat(offset).isAtMost(1024);
+ fOutput.write(dataBuffer, 0, (int) offset);
+ int readLength = 0;
+ while ((readLength = is.read(dataBuffer)) != -1) {
+ fOutput.write(dataBuffer, 0, readLength);
+ }
+ is.close();
+ fOutput.close();
+ return file;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapShaderTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapShaderTest.java
new file mode 100644
index 000000000..3210b43ae
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapShaderTest.java
@@ -0,0 +1,150 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Shader;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeBitmapShaderTest {
+ private static final int TILE_WIDTH = 20;
+ private static final int TILE_HEIGHT = 20;
+ private static final int BORDER_WIDTH = 5;
+ private static final int BORDER_COLOR = Color.BLUE;
+ private static final int CENTER_COLOR = Color.RED;
+ private static final int NUM_TILES = 4;
+
+ @Test
+ public void testBitmapShader() {
+ Bitmap tile = Bitmap.createBitmap(TILE_WIDTH, TILE_HEIGHT, Bitmap.Config.ARGB_8888);
+ tile.eraseColor(BORDER_COLOR);
+ Canvas c = new Canvas(tile);
+ Paint p = new Paint();
+ p.setColor(CENTER_COLOR);
+ c.drawRect(
+ BORDER_WIDTH, BORDER_WIDTH, TILE_WIDTH - BORDER_WIDTH, TILE_HEIGHT - BORDER_WIDTH, p);
+ BitmapShader shader = new BitmapShader(tile, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+ Paint paint = new Paint();
+ paint.setShader(shader);
+ // create a bitmap that fits (NUM_TILES - 0.5) tiles in both directions
+ Bitmap b =
+ Bitmap.createBitmap(
+ NUM_TILES * TILE_WIDTH - TILE_WIDTH / 2,
+ NUM_TILES * TILE_HEIGHT - TILE_HEIGHT / 2,
+ Bitmap.Config.ARGB_8888);
+ b.eraseColor(Color.BLACK);
+ Canvas canvas = new Canvas(b);
+ canvas.drawPaint(paint);
+
+ for (int y = 0; y < NUM_TILES; y++) {
+ for (int x = 0; x < NUM_TILES; x++) {
+ verifyTile(b, x * TILE_WIDTH, y * TILE_HEIGHT);
+ }
+ }
+ }
+
+ /** Check the colors of the tile at the given coordinates in the given bitmap. */
+ private void verifyTile(Bitmap bitmap, int tileX, int tileY) {
+ for (int y = 0; y < TILE_HEIGHT; y++) {
+ for (int x = 0; x < TILE_WIDTH; x++) {
+ if (x < BORDER_WIDTH
+ || x >= TILE_WIDTH - BORDER_WIDTH
+ || y < BORDER_WIDTH
+ || y >= TILE_HEIGHT - BORDER_WIDTH) {
+ verifyColor(BORDER_COLOR, bitmap, x + tileX, y + tileY);
+ } else {
+ verifyColor(CENTER_COLOR, bitmap, x + tileX, y + tileY);
+ }
+ }
+ }
+ }
+
+ /**
+ * Asserts that the pixel at the given coordinates in the given bitmap matches the given color.
+ * Simply returns if the coordinates are outside the bitmap area.
+ */
+ private void verifyColor(int color, Bitmap bitmap, int x, int y) {
+ if (x < bitmap.getWidth() && y < bitmap.getHeight()) {
+ assertEquals(color, bitmap.getPixel(x, y));
+ }
+ }
+
+ @Test
+ public void testClamp() {
+ Bitmap bitmap = Bitmap.createBitmap(2, 1, Bitmap.Config.ARGB_8888);
+ bitmap.setPixel(0, 0, Color.RED);
+ bitmap.setPixel(1, 0, Color.BLUE);
+
+ BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+
+ Bitmap dstBitmap = Bitmap.createBitmap(4, 1, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(dstBitmap);
+ Paint paint = new Paint();
+ paint.setShader(shader);
+ canvas.drawRect(0, 0, 4, 1, paint);
+ canvas.setBitmap(null);
+
+ int[] pixels = new int[4];
+ dstBitmap.getPixels(pixels, 0, 4, 0, 0, 4, 1);
+ Assert.assertArrayEquals(new int[] {Color.RED, Color.BLUE, Color.BLUE, Color.BLUE}, pixels);
+ }
+
+ @Test
+ public void testRepeat() {
+ Bitmap bitmap = Bitmap.createBitmap(2, 1, Bitmap.Config.ARGB_8888);
+ bitmap.setPixel(0, 0, Color.RED);
+ bitmap.setPixel(1, 0, Color.BLUE);
+
+ BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+
+ Bitmap dstBitmap = Bitmap.createBitmap(4, 1, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(dstBitmap);
+ Paint paint = new Paint();
+ paint.setShader(shader);
+ canvas.drawRect(0, 0, 4, 1, paint);
+ canvas.setBitmap(null);
+
+ int[] pixels = new int[4];
+ dstBitmap.getPixels(pixels, 0, 4, 0, 0, 4, 1);
+ Assert.assertArrayEquals(new int[] {Color.RED, Color.BLUE, Color.RED, Color.BLUE}, pixels);
+ }
+
+ @Test
+ public void testMirror() {
+ Bitmap bitmap = Bitmap.createBitmap(2, 1, Bitmap.Config.ARGB_8888);
+ bitmap.setPixel(0, 0, Color.RED);
+ bitmap.setPixel(1, 0, Color.BLUE);
+
+ BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);
+
+ Bitmap dstBitmap = Bitmap.createBitmap(4, 1, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(dstBitmap);
+ Paint paint = new Paint();
+ paint.setShader(shader);
+ canvas.drawRect(0, 0, 4, 1, paint);
+ canvas.setBitmap(null);
+
+ int[] pixels = new int[4];
+ dstBitmap.getPixels(pixels, 0, 4, 0, 0, 4, 1);
+ Assert.assertArrayEquals(new int[] {Color.RED, Color.BLUE, Color.BLUE, Color.RED}, pixels);
+ }
+
+ @Test
+ public void testNullBitmap() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new BitmapShader(null, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapTest.java
new file mode 100644
index 000000000..0aefa132e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBitmapTest.java
@@ -0,0 +1,1763 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorSpace;
+import android.graphics.ColorSpace.Named;
+import android.graphics.Paint;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+import android.os.StrictMode;
+import android.util.DisplayMetrics;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowBitmap;
+import org.robolectric.shadows.ShadowNativeBitmap;
+
+@org.robolectric.annotation.Config(minSdk = O)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeBitmapTest {
+ // small alpha values cause color values to be pre-multiplied down, losing accuracy
+ private static final int PREMUL_COLOR = Color.argb(2, 255, 254, 253);
+ private static final int PREMUL_ROUNDED_COLOR = Color.argb(2, 255, 255, 255);
+ private static final int PREMUL_STORED_COLOR = Color.argb(2, 2, 2, 2);
+
+ private static final BitmapFactory.Options HARDWARE_OPTIONS = createHardwareBitmapOptions();
+
+ private Resources res;
+ private Bitmap bitmap;
+ private BitmapFactory.Options options;
+
+ public static List<ColorSpace> getRgbColorSpaces() {
+ List<ColorSpace> rgbColorSpaces = new ArrayList<ColorSpace>();
+ for (ColorSpace.Named e : ColorSpace.Named.values()) {
+ ColorSpace cs = ColorSpace.get(e);
+ if (cs.getModel() != ColorSpace.Model.RGB) {
+ continue;
+ }
+ if (((ColorSpace.Rgb) cs).getTransferParameters() == null) {
+ continue;
+ }
+ rgbColorSpaces.add(cs);
+ }
+ return rgbColorSpaces;
+ }
+
+ @Before
+ public void setup() {
+ res = RuntimeEnvironment.getApplication().getResources();
+ options = new BitmapFactory.Options();
+ options.inScaled = false;
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+ }
+
+ @Test
+ public void testCompressRecycled() {
+ bitmap.recycle();
+ assertThrows(IllegalStateException.class, () -> bitmap.compress(CompressFormat.JPEG, 0, null));
+ }
+
+ @Test
+ public void testCompressNullStream() {
+ assertThrows(NullPointerException.class, () -> bitmap.compress(CompressFormat.JPEG, 0, null));
+ }
+
+ @Test
+ public void testCompressQualityTooLow() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> bitmap.compress(CompressFormat.JPEG, -1, new ByteArrayOutputStream()));
+ }
+
+ @Test
+ public void testCompressQualityTooHigh() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> bitmap.compress(CompressFormat.JPEG, 101, new ByteArrayOutputStream()));
+ }
+
+ @Test
+ public void testCopyRecycled() {
+ bitmap.recycle();
+ assertThrows(IllegalStateException.class, () -> bitmap.copy(Config.RGB_565, false));
+ }
+
+ @Test
+ public void testCopyConfigs() {
+ Config[] supportedConfigs =
+ new Config[] {
+ Config.ALPHA_8, Config.RGB_565, Config.ARGB_8888, Config.RGBA_F16,
+ };
+ for (Config src : supportedConfigs) {
+ for (Config dst : supportedConfigs) {
+ Bitmap srcBitmap = Bitmap.createBitmap(1, 1, src);
+ srcBitmap.eraseColor(Color.WHITE);
+ Bitmap dstBitmap = srcBitmap.copy(dst, false);
+ assertNotNull("Should support copying from " + src + " to " + dst, dstBitmap);
+ if (Config.ALPHA_8 == dst || Config.ALPHA_8 == src) {
+ // Color will be opaque but color information will be lost.
+ assertEquals(
+ "Color should be black when copying from " + src + " to " + dst,
+ Color.BLACK,
+ dstBitmap.getPixel(0, 0));
+ } else {
+ assertEquals(
+ "Color should be preserved when copying from " + src + " to " + dst,
+ Color.WHITE,
+ dstBitmap.getPixel(0, 0));
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testCopyMutableHwBitmap() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.copy(Config.HARDWARE, true));
+ }
+
+ @Test
+ public void testCopyPixelsToBufferUnsupportedBufferClass() {
+ final int pixSize = bitmap.getRowBytes() * bitmap.getHeight();
+
+ assertThrows(
+ RuntimeException.class, () -> bitmap.copyPixelsToBuffer(CharBuffer.allocate(pixSize)));
+ }
+
+ @Test
+ public void testCopyPixelsToBufferBufferTooSmall() {
+ final int pixSize = bitmap.getRowBytes() * bitmap.getHeight();
+ final int tooSmall = pixSize / 2;
+
+ assertThrows(
+ RuntimeException.class, () -> bitmap.copyPixelsToBuffer(ByteBuffer.allocate(tooSmall)));
+ }
+
+ @Test
+ public void testCreateBitmap1() {
+ int[] colors = createColors(100);
+ Bitmap bitmap = Bitmap.createBitmap(colors, 10, 10, Config.RGB_565);
+ assertFalse(bitmap.isMutable());
+ Bitmap ret = Bitmap.createBitmap(bitmap);
+ assertNotNull(ret);
+ assertFalse(ret.isMutable());
+ assertEquals(10, ret.getWidth());
+ assertEquals(10, ret.getHeight());
+ assertEquals(Config.RGB_565, ret.getConfig());
+ }
+
+ @Test
+ public void testCreateBitmapNegativeX() {
+ assertThrows(
+ IllegalArgumentException.class, () -> Bitmap.createBitmap(bitmap, -100, 50, 50, 200));
+ }
+
+ @Test
+ public void testCreateBitmapNegativeXY() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+
+ // abnormal case: x and/or y less than 0
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Bitmap.createBitmap(bitmap, -1, -1, 10, 10, null, false));
+ }
+
+ @Test
+ public void testCreateBitmapNegativeWidthHeight() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+
+ // abnormal case: width and/or height less than 0
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Bitmap.createBitmap(bitmap, 1, 1, -10, -10, null, false));
+ }
+
+ @Test
+ public void testCreateBitmapXRegionTooWide() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+
+ // abnormal case: (x + width) bigger than source bitmap's width
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Bitmap.createBitmap(bitmap, 10, 10, 95, 50, null, false));
+ }
+
+ @Test
+ public void testCreateBitmapYRegionTooTall() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+
+ // abnormal case: (y + height) bigger than source bitmap's height
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Bitmap.createBitmap(bitmap, 10, 10, 50, 95, null, false));
+ }
+
+ @Test
+ public void testCreateMutableBitmapWithHardwareConfig() {
+ assertThrows(
+ IllegalArgumentException.class, () -> Bitmap.createBitmap(100, 100, Config.HARDWARE));
+ }
+
+ @Test
+ public void testCreateBitmap4() {
+ Bitmap ret = Bitmap.createBitmap(100, 200, Config.RGB_565);
+ assertNotNull(ret);
+ assertTrue(ret.isMutable());
+ assertEquals(100, ret.getWidth());
+ assertEquals(200, ret.getHeight());
+ assertEquals(Config.RGB_565, ret.getConfig());
+ }
+
+ @Test
+ public void testCreateBitmapFromColorsNegativeWidthHeight() {
+ int[] colors = createColors(100);
+
+ // abnormal case: width and/or height less than 0
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Bitmap.createBitmap(colors, 0, 100, -1, 100, Config.RGB_565));
+ }
+
+ @Test
+ public void testCreateBitmapFromColorsIllegalStride() {
+ int[] colors = createColors(100);
+
+ // abnormal case: stride less than width and bigger than -width
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Bitmap.createBitmap(colors, 10, 10, 100, 100, Config.RGB_565));
+ }
+
+ @Test
+ public void testCreateBitmapFromColorsNegativeOffset() {
+ int[] colors = createColors(100);
+
+ // abnormal case: offset less than 0
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> Bitmap.createBitmap(colors, -10, 100, 100, 100, Config.RGB_565));
+ }
+
+ @Test
+ public void testCreateBitmapFromColorsOffsetTooLarge() {
+ int[] colors = createColors(100);
+
+ // abnormal case: (offset + width) bigger than colors' length
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> Bitmap.createBitmap(colors, 10, 100, 100, 100, Config.RGB_565));
+ }
+
+ @Test
+ public void testCreateBitmapFromColorsScalnlineTooLarge() {
+ int[] colors = createColors(100);
+
+ // abnormal case: (lastScanline + width) bigger than colors' length
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> Bitmap.createBitmap(colors, 10, 100, 50, 100, Config.RGB_565));
+ }
+
+ @Test
+ public void testCreateBitmap6() {
+ int[] colors = createColors(100);
+
+ // normal case
+ Bitmap ret = Bitmap.createBitmap(colors, 5, 10, 10, 5, Config.RGB_565);
+ assertNotNull(ret);
+ assertFalse(ret.isMutable());
+ assertEquals(10, ret.getWidth());
+ assertEquals(5, ret.getHeight());
+ assertEquals(Config.RGB_565, ret.getConfig());
+ }
+
+ @Test
+ public void testCreateBitmap_displayMetrics_mutable() {
+ DisplayMetrics metrics = RuntimeEnvironment.getApplication().getResources().getDisplayMetrics();
+
+ Bitmap bitmap;
+ bitmap = Bitmap.createBitmap(metrics, 10, 10, Config.ARGB_8888);
+ assertTrue(bitmap.isMutable());
+ assertEquals(metrics.densityDpi, bitmap.getDensity());
+
+ bitmap = Bitmap.createBitmap(metrics, 10, 10, Config.ARGB_8888);
+ assertTrue(bitmap.isMutable());
+ assertEquals(metrics.densityDpi, bitmap.getDensity());
+
+ bitmap = Bitmap.createBitmap(metrics, 10, 10, Config.ARGB_8888, true);
+ assertTrue(bitmap.isMutable());
+ assertEquals(metrics.densityDpi, bitmap.getDensity());
+
+ bitmap =
+ Bitmap.createBitmap(
+ metrics, 10, 10, Config.ARGB_8888, true, ColorSpace.get(ColorSpace.Named.SRGB));
+
+ assertTrue(bitmap.isMutable());
+ assertEquals(metrics.densityDpi, bitmap.getDensity());
+
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(metrics, colors, 0, 10, 10, 10, Config.ARGB_8888);
+ assertNotNull(bitmap);
+ assertFalse(bitmap.isMutable());
+
+ bitmap = Bitmap.createBitmap(metrics, colors, 10, 10, Config.ARGB_8888);
+ assertNotNull(bitmap);
+ assertFalse(bitmap.isMutable());
+ }
+
+ @Test
+ public void testCreateBitmap_noDisplayMetrics_mutable() {
+ Bitmap bitmap;
+ bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+ assertTrue(bitmap.isMutable());
+
+ bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888, true);
+ assertTrue(bitmap.isMutable());
+
+ bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888, true, ColorSpace.get(Named.SRGB));
+ assertTrue(bitmap.isMutable());
+ }
+
+ @Test
+ public void testCreateBitmap_displayMetrics_immutable() {
+ DisplayMetrics metrics = RuntimeEnvironment.getApplication().getResources().getDisplayMetrics();
+ int[] colors = createColors(100);
+
+ Bitmap bitmap;
+ bitmap = Bitmap.createBitmap(metrics, colors, 0, 10, 10, 10, Config.ARGB_8888);
+ assertFalse(bitmap.isMutable());
+ assertEquals(metrics.densityDpi, bitmap.getDensity());
+
+ bitmap = Bitmap.createBitmap(metrics, colors, 10, 10, Config.ARGB_8888);
+ assertFalse(bitmap.isMutable());
+ assertEquals(metrics.densityDpi, bitmap.getDensity());
+ }
+
+ @Test
+ public void testCreateBitmap_noDisplayMetrics_immutable() {
+ int[] colors = createColors(100);
+ Bitmap bitmap;
+ bitmap = Bitmap.createBitmap(colors, 0, 10, 10, 10, Config.ARGB_8888);
+ assertFalse(bitmap.isMutable());
+
+ bitmap = Bitmap.createBitmap(colors, 10, 10, Config.ARGB_8888);
+ assertFalse(bitmap.isMutable());
+ }
+
+ @SuppressWarnings("UnusedVariable")
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testWrapHardwareBufferWithInvalidUsageFails() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ try (HardwareBuffer hwBuffer =
+ HardwareBuffer.create(
+ 512, 512, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_CPU_WRITE_RARELY)) {
+ Bitmap bitmap = Bitmap.wrapHardwareBuffer(hwBuffer, ColorSpace.get(Named.SRGB));
+ }
+ });
+ }
+
+ @SuppressWarnings("UnusedVariable")
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testWrapHardwareBufferWithRgbBufferButNonRgbColorSpaceFails() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ try (HardwareBuffer hwBuffer =
+ HardwareBuffer.create(
+ 512, 512, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)) {
+ Bitmap bitmap = Bitmap.wrapHardwareBuffer(hwBuffer, ColorSpace.get(Named.CIE_LAB));
+ }
+ });
+ }
+
+ @Test
+ public void testGenerationId() {
+ Bitmap bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+ int genId = bitmap.getGenerationId();
+ assertEquals("not expected to change", genId, bitmap.getGenerationId());
+ bitmap.setDensity(bitmap.getDensity() + 4);
+ assertEquals("not expected to change", genId, bitmap.getGenerationId());
+ bitmap.getPixel(0, 0);
+ assertEquals("not expected to change", genId, bitmap.getGenerationId());
+
+ int beforeGenId = bitmap.getGenerationId();
+ bitmap.eraseColor(Color.WHITE);
+ int afterGenId = bitmap.getGenerationId();
+ assertTrue("expected to increase", afterGenId > beforeGenId);
+
+ beforeGenId = bitmap.getGenerationId();
+ bitmap.setPixel(4, 4, Color.BLUE);
+ afterGenId = bitmap.getGenerationId();
+ assertTrue("expected to increase again", afterGenId > beforeGenId);
+ }
+
+ @Test
+ public void testDescribeContents() {
+ assertEquals(0, bitmap.describeContents());
+ }
+
+ @Test
+ public void testEraseColorOnRecycled() {
+ bitmap.recycle();
+
+ assertThrows(IllegalStateException.class, () -> bitmap.eraseColor(0));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testEraseColorLongOnRecycled() {
+ bitmap.recycle();
+
+ assertThrows(IllegalStateException.class, () -> bitmap.eraseColor(Color.pack(0)));
+ }
+
+ @Test
+ public void testEraseColorOnImmutable() {
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+
+ // abnormal case: bitmap is immutable
+ assertThrows(IllegalStateException.class, () -> bitmap.eraseColor(0));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testEraseColorLongOnImmutable() {
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+
+ // abnormal case: bitmap is immutable
+ assertThrows(IllegalStateException.class, () -> bitmap.eraseColor(Color.pack(0)));
+ }
+
+ @Test
+ public void testEraseColor() {
+ // normal case
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ bitmap.eraseColor(0xffff0000);
+ assertEquals(0xffff0000, bitmap.getPixel(10, 10));
+ assertEquals(0xffff0000, bitmap.getPixel(50, 50));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testGetColorOOB() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.getColor(-1, 0));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testGetColorOOB2() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.getColor(5, -10));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testGetColorOOB3() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.getColor(100, 10));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testGetColorOOB4() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.getColor(99, 1000));
+ }
+
+ @Test
+ @org.robolectric.annotation.Config(minSdk = Q)
+ public void testGetColorRecycled() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ bitmap.recycle();
+ assertThrows(IllegalStateException.class, () -> bitmap.getColor(0, 0));
+ }
+
+ private static float clamp(float f) {
+ return clamp(f, 0.0f, 1.0f);
+ }
+
+ private static float clamp(float f, float min, float max) {
+ return Math.min(Math.max(f, min), max);
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testGetColor() {
+ final ColorSpace sRGB = ColorSpace.get(ColorSpace.Named.SRGB);
+ List<ColorSpace> rgbColorSpaces = getRgbColorSpaces();
+ for (Config config : new Config[] {Config.ARGB_8888, Config.RGBA_F16, Config.RGB_565}) {
+ for (ColorSpace bitmapColorSpace : rgbColorSpaces) {
+ bitmap = Bitmap.createBitmap(1, 1, config, /*hasAlpha*/ false, bitmapColorSpace);
+ bitmapColorSpace = bitmap.getColorSpace();
+ for (ColorSpace eraseColorSpace : rgbColorSpaces) {
+ for (long wideGamutLong :
+ new long[] {
+ Color.pack(1.0f, 0.0f, 0.0f, 1.0f, eraseColorSpace),
+ Color.pack(0.0f, 1.0f, 0.0f, 1.0f, eraseColorSpace),
+ Color.pack(0.0f, 0.0f, 1.0f, 1.0f, eraseColorSpace)
+ }) {
+ bitmap.eraseColor(wideGamutLong);
+
+ Color result = bitmap.getColor(0, 0);
+ if (bitmap.getColorSpace().equals(sRGB)) {
+ assertEquals(bitmap.getPixel(0, 0), result.toArgb());
+ }
+ if (eraseColorSpace.equals(bitmapColorSpace)) {
+ final Color wideGamutColor = Color.valueOf(wideGamutLong);
+ ColorUtils.verifyColor(
+ "Erasing to Bitmap's ColorSpace " + bitmapColorSpace,
+ wideGamutColor,
+ result,
+ .001f);
+
+ } else {
+ Color convertedColor = Color.valueOf(Color.convert(wideGamutLong, bitmapColorSpace));
+ if (bitmap.getConfig() != Config.RGBA_F16) {
+ // It's possible that we have to clip to fit into the Config.
+ convertedColor =
+ Color.valueOf(
+ clamp(convertedColor.red()),
+ clamp(convertedColor.green()),
+ clamp(convertedColor.blue()),
+ convertedColor.alpha(),
+ bitmapColorSpace);
+ }
+ ColorUtils.verifyColor(
+ "Bitmap(Config: "
+ + bitmap.getConfig()
+ + ", ColorSpace: "
+ + bitmapColorSpace
+ + ") erasing to "
+ + Color.valueOf(wideGamutLong),
+ convertedColor,
+ result,
+ .03f);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static class ARGB {
+ public final float alpha;
+ public final float red;
+ public final float green;
+ public final float blue;
+
+ ARGB(float alpha, float red, float green, float blue) {
+ this.alpha = alpha;
+ this.red = red;
+ this.green = green;
+ this.blue = blue;
+ }
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testEraseColorLong() {
+ List<ColorSpace> rgbColorSpaces = getRgbColorSpaces();
+ for (Config config : new Config[] {Config.ARGB_8888, Config.RGB_565, Config.RGBA_F16}) {
+ bitmap = Bitmap.createBitmap(100, 100, config);
+ // pack SRGB colors into ColorLongs.
+ for (int color :
+ new int[] {
+ Color.RED, Color.BLUE, Color.GREEN, Color.BLACK, Color.WHITE, Color.TRANSPARENT
+ }) {
+ if (config.equals(Config.RGB_565) && Float.compare(Color.alpha(color), 1.0f) != 0) {
+ // 565 doesn't support alpha.
+ continue;
+ }
+ bitmap.eraseColor(Color.pack(color));
+ // The Bitmap is either SRGB or SRGBLinear (F16). getPixel(), which retrieves the
+ // color in SRGB, should match exactly.
+ ColorUtils.verifyColor(
+ "Config " + config + " mismatch at 10, 10 ", color, bitmap.getPixel(10, 10), 0);
+ ColorUtils.verifyColor(
+ "Config " + config + " mismatch at 50, 50 ", color, bitmap.getPixel(50, 50), 0);
+ }
+
+ // Use arbitrary colors in various ColorSpaces. getPixel() should approximately match
+ // the SRGB version of the color.
+ for (ARGB color :
+ new ARGB[] {
+ new ARGB(1.0f, .5f, .5f, .5f),
+ new ARGB(1.0f, .3f, .6f, .9f),
+ new ARGB(0.5f, .2f, .8f, .7f)
+ }) {
+ if (config.equals(Config.RGB_565) && Float.compare(color.alpha, 1.0f) != 0) {
+ continue;
+ }
+ int srgbColor = Color.argb(color.alpha, color.red, color.green, color.blue);
+ for (ColorSpace cs : rgbColorSpaces) {
+ long longColor = Color.convert(srgbColor, cs);
+ bitmap.eraseColor(longColor);
+ // These tolerances were chosen by trial and error. It is expected that
+ // some conversions do not round-trip perfectly.
+ int tolerance = 1;
+ if (config.equals(Config.RGB_565)) {
+ tolerance = 4;
+ } else if (cs.equals(ColorSpace.get(ColorSpace.Named.SMPTE_C))) {
+ tolerance = 3;
+ }
+
+ ColorUtils.verifyColor(
+ "Config " + config + ", ColorSpace " + cs + ", mismatch at 10, 10 ",
+ srgbColor,
+ bitmap.getPixel(10, 10),
+ tolerance);
+ ColorUtils.verifyColor(
+ "Config " + config + ", ColorSpace " + cs + ", mismatch at 50, 50 ",
+ srgbColor,
+ bitmap.getPixel(50, 50),
+ tolerance);
+ }
+ }
+ }
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testEraseColorOnP3() {
+ // Use a ColorLong with a different ColorSpace than the Bitmap. getPixel() should
+ // approximately match the SRGB version of the color.
+ bitmap =
+ Bitmap.createBitmap(
+ 100, 100, Config.ARGB_8888, true, ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
+ int srgbColor = Color.argb(.5f, .3f, .6f, .7f);
+ long acesColor = Color.convert(srgbColor, ColorSpace.get(ColorSpace.Named.ACES));
+ bitmap.eraseColor(acesColor);
+ ColorUtils.verifyColor("Mismatch at 15, 15", srgbColor, bitmap.getPixel(15, 15), 1);
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testEraseColorXYZ() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ bitmap.eraseColor(Color.convert(Color.BLUE, ColorSpace.get(ColorSpace.Named.CIE_XYZ))));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testEraseColorLAB() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ bitmap.eraseColor(Color.convert(Color.BLUE, ColorSpace.get(ColorSpace.Named.CIE_LAB))));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testEraseColorUnknown() {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.eraseColor(-1L));
+ }
+
+ @Test
+ public void testExtractAlphaFromRecycled() {
+ bitmap.recycle();
+
+ assertThrows(IllegalStateException.class, () -> bitmap.extractAlpha());
+ }
+
+ @Test
+ public void testExtractAlpha() {
+ // normal case
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+ Bitmap ret = bitmap.extractAlpha();
+ assertNotNull(ret);
+ int source = bitmap.getPixel(10, 20);
+ int result = ret.getPixel(10, 20);
+ assertEquals(Color.alpha(source), Color.alpha(result));
+ assertEquals(0xFF, Color.alpha(result));
+ }
+
+ @Test
+ public void testExtractAlphaWithPaintAndOffsetFromRecycled() {
+ bitmap.recycle();
+
+ assertThrows(
+ IllegalStateException.class, () -> bitmap.extractAlpha(new Paint(), new int[] {0, 1}));
+ }
+
+ @Test
+ public void testGetAllocationByteCount() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
+ int alloc = bitmap.getAllocationByteCount();
+ assertEquals(bitmap.getByteCount(), alloc);
+
+ // reconfigure same size
+ bitmap.reconfigure(50, 100, Bitmap.Config.ARGB_8888);
+ assertEquals(bitmap.getByteCount(), alloc);
+ assertEquals(bitmap.getAllocationByteCount(), alloc);
+
+ // reconfigure different size
+ bitmap.reconfigure(10, 10, Bitmap.Config.ALPHA_8);
+ assertEquals(100, bitmap.getByteCount());
+ assertEquals(bitmap.getAllocationByteCount(), alloc);
+ }
+
+ @Test
+ public void testGetHeight() {
+ assertEquals(31, bitmap.getHeight());
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
+ assertEquals(200, bitmap.getHeight());
+ }
+
+ @Test
+ public void testGetNinePatchChunk() {
+ assertNull(bitmap.getNinePatchChunk());
+ }
+
+ @Test
+ public void testGetPixelFromRecycled() {
+ bitmap.recycle();
+
+ assertThrows(IllegalStateException.class, () -> bitmap.getPixel(10, 16));
+ }
+
+ @Test
+ public void testGetPixelXTooLarge() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+
+ // abnormal case: x bigger than the source bitmap's width
+ assertThrows(IllegalArgumentException.class, () -> bitmap.getPixel(200, 16));
+ }
+
+ @Test
+ public void testGetPixelYTooLarge() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+
+ // abnormal case: y bigger than the source bitmap's height
+ assertThrows(IllegalArgumentException.class, () -> bitmap.getPixel(10, 300));
+ }
+
+ @Test
+ public void testGetPixel() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+
+ // normal case 565
+ bitmap.setPixel(10, 16, 0xFF << 24);
+ assertEquals(0xFF << 24, bitmap.getPixel(10, 16));
+
+ // normal case A_8
+ bitmap = Bitmap.createBitmap(10, 10, Config.ALPHA_8);
+ bitmap.setPixel(5, 5, 0xFFFFFFFF);
+ assertEquals(0xFF000000, bitmap.getPixel(5, 5));
+ bitmap.setPixel(5, 5, 0xA8A8A8A8);
+ assertEquals(0xA8000000, bitmap.getPixel(5, 5));
+ bitmap.setPixel(5, 5, 0x00000000);
+ assertEquals(0x00000000, bitmap.getPixel(5, 5));
+ bitmap.setPixel(5, 5, 0x1F000000);
+ assertEquals(0x1F000000, bitmap.getPixel(5, 5));
+ }
+
+ @Test
+ public void testGetRowBytes() {
+ Bitmap bm0 = Bitmap.createBitmap(100, 200, Bitmap.Config.ALPHA_8);
+ Bitmap bm1 = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
+ Bitmap bm2 = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+ Bitmap bm3 = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_4444);
+
+ assertEquals(100, bm0.getRowBytes());
+ assertEquals(400, bm1.getRowBytes());
+ assertEquals(200, bm2.getRowBytes());
+ // Attempting to create a 4444 bitmap actually creates an 8888 bitmap.
+ assertEquals(400, bm3.getRowBytes());
+ }
+
+ @Test
+ public void testGetWidth() {
+ assertEquals(31, bitmap.getWidth());
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
+ assertEquals(100, bitmap.getWidth());
+ }
+
+ @Test
+ public void testHasAlpha() {
+ assertFalse(bitmap.hasAlpha());
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
+ assertTrue(bitmap.hasAlpha());
+ }
+
+ @Test
+ public void testIsMutable() {
+ assertFalse(bitmap.isMutable());
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ assertTrue(bitmap.isMutable());
+ }
+
+ @Test
+ public void testIsRecycled() {
+ assertFalse(bitmap.isRecycled());
+ bitmap.recycle();
+ assertTrue(bitmap.isRecycled());
+ }
+
+ @Test
+ public void testReconfigure() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+ int alloc = bitmap.getAllocationByteCount();
+
+ // test shrinking
+ bitmap.reconfigure(50, 100, Bitmap.Config.ALPHA_8);
+ assertEquals(bitmap.getAllocationByteCount(), alloc);
+ assertEquals(bitmap.getByteCount() * 8, alloc);
+ }
+
+ @Test
+ public void testReconfigureExpanding() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> bitmap.reconfigure(101, 201, Bitmap.Config.ARGB_8888));
+ }
+
+ @Test
+ public void testReconfigureMutable() {
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+ assertThrows(
+ IllegalStateException.class, () -> bitmap.reconfigure(1, 1, Bitmap.Config.ALPHA_8));
+ }
+
+ // Used by testAlphaAndPremul.
+ private static final Config[] CONFIGS =
+ new Config[] {Config.ALPHA_8, Config.ARGB_4444, Config.ARGB_8888, Config.RGB_565};
+
+ // test that reconfigure, setHasAlpha, and setPremultiplied behave as expected with
+ // respect to alpha and premultiplied.
+ @Test
+ public void testAlphaAndPremul() {
+ boolean[] falseTrue = new boolean[] {false, true};
+ for (Config fromConfig : CONFIGS) {
+ for (Config toConfig : CONFIGS) {
+ for (boolean hasAlpha : falseTrue) {
+ for (boolean isPremul : falseTrue) {
+ Bitmap bitmap = Bitmap.createBitmap(10, 10, fromConfig);
+
+ // 4444 is deprecated, and will convert to 8888. No need to
+ // attempt a reconfigure, which will be tested when fromConfig
+ // is 8888.
+ if (fromConfig == Config.ARGB_4444) {
+ assertEquals(Config.ARGB_8888, bitmap.getConfig());
+ break;
+ }
+
+ bitmap.setHasAlpha(hasAlpha);
+ bitmap.setPremultiplied(isPremul);
+
+ verifyAlphaAndPremul(bitmap, hasAlpha, isPremul, false);
+
+ // reconfigure to a smaller size so the function will still succeed when
+ // going to a Config that requires more bits.
+ bitmap.reconfigure(1, 1, toConfig);
+ if (toConfig == Config.ARGB_4444) {
+ assertEquals(Config.ARGB_8888, bitmap.getConfig());
+ } else {
+ assertEquals(toConfig, bitmap.getConfig());
+ }
+
+ // Check that the alpha and premultiplied state has not changed (unless
+ // we expected it to).
+ verifyAlphaAndPremul(bitmap, hasAlpha, isPremul, fromConfig == Config.RGB_565);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Assert that bitmap returns the appropriate values for hasAlpha() and isPremultiplied().
+ *
+ * @param bitmap Bitmap to check.
+ * @param expectedAlpha Expected return value from bitmap.hasAlpha(). Note that this is based on
+ * what was set, but may be different from the actual return value depending on the Config and
+ * convertedFrom565.
+ * @param expectedPremul Expected return value from bitmap.isPremultiplied(). Similar to
+ * expectedAlpha, this is based on what was set, but may be different from the actual return
+ * value depending on the Config.
+ * @param convertedFrom565 Whether bitmap was converted to its current Config by being
+ * reconfigured from RGB_565. If true, and bitmap is now a Config that supports alpha,
+ * hasAlpha() is expected to be true even if expectedAlpha is false.
+ */
+ @SuppressWarnings("MissingCasesInEnumSwitch")
+ private void verifyAlphaAndPremul(
+ Bitmap bitmap, boolean expectedAlpha, boolean expectedPremul, boolean convertedFrom565) {
+ switch (bitmap.getConfig()) {
+ case ARGB_4444:
+ // This shouldn't happen, since we don't allow creating or converting
+ // to 4444.
+ assertFalse(true);
+ break;
+ case RGB_565:
+ assertFalse(bitmap.hasAlpha());
+ assertFalse(bitmap.isPremultiplied());
+ break;
+ case ALPHA_8:
+ // ALPHA_8 behaves mostly the same as 8888, except for premultiplied. Fall through.
+ case ARGB_8888:
+ // Since 565 is necessarily opaque, we revert to hasAlpha when switching to a type
+ // that can have alpha.
+ if (convertedFrom565) {
+ assertTrue(bitmap.hasAlpha());
+ } else {
+ assertEquals(expectedAlpha, bitmap.hasAlpha());
+ }
+
+ if (bitmap.hasAlpha()) {
+ // ALPHA_8's premultiplied status is undefined.
+ if (bitmap.getConfig() != Config.ALPHA_8) {
+ assertEquals(expectedPremul, bitmap.isPremultiplied());
+ }
+ } else {
+ // Opaque bitmap is never considered premultiplied.
+ assertFalse(bitmap.isPremultiplied());
+ }
+ break;
+ }
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpace() {
+ // Use arbitrary colors and assign to various ColorSpaces.
+ for (ARGB color :
+ new ARGB[] {
+ new ARGB(1.0f, .5f, .5f, .5f),
+ new ARGB(1.0f, .3f, .6f, .9f),
+ new ARGB(0.5f, .2f, .8f, .7f)
+ }) {
+
+ int srgbColor = Color.argb(color.alpha, color.red, color.green, color.blue);
+ for (ColorSpace cs : getRgbColorSpaces()) {
+ for (Config config :
+ new Config[] {
+ // F16 is tested elsewhere, since it defaults to EXTENDED_SRGB, and
+ // many of these calls to setColorSpace would reduce the range, resulting
+ // in an Exception.
+ Config.ARGB_8888, Config.RGB_565,
+ }) {
+ bitmap = Bitmap.createBitmap(10, 10, config);
+ bitmap.eraseColor(srgbColor);
+ bitmap.setColorSpace(cs);
+ ColorSpace actual = bitmap.getColorSpace();
+ if (Objects.equals(cs, ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB))) {
+ assertSame(ColorSpace.get(ColorSpace.Named.SRGB), actual);
+ } else if (Objects.equals(cs, ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB))) {
+ assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_SRGB), actual);
+ } else {
+ assertSame(cs, actual);
+ }
+
+ // This tolerance was chosen by trial and error. It is expected that
+ // some conversions do not round-trip perfectly.
+ int tolerance = 2;
+ Color c = Color.valueOf(color.red, color.green, color.blue, color.alpha, cs);
+ ColorUtils.verifyColor(
+ "Mismatch after setting the colorSpace to " + cs.getName(),
+ c.convert(bitmap.getColorSpace()),
+ bitmap.getColor(5, 5),
+ tolerance);
+ }
+ }
+ }
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceRecycled() {
+ bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+ bitmap.recycle();
+ assertThrows(
+ IllegalStateException.class, () -> bitmap.setColorSpace(ColorSpace.get(Named.DISPLAY_P3)));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceNull() {
+ bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.setColorSpace(null));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceXYZ() {
+ bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+ assertThrows(
+ IllegalArgumentException.class, () -> bitmap.setColorSpace(ColorSpace.get(Named.CIE_XYZ)));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceNoTransferParameters() {
+ bitmap = Bitmap.createBitmap(10, 10, Config.ARGB_8888);
+ ColorSpace cs =
+ new ColorSpace.Rgb(
+ "NoTransferParams",
+ new float[] {0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f},
+ ColorSpace.ILLUMINANT_D50,
+ x -> Math.pow(x, 1.0f / 2.2f),
+ x -> Math.pow(x, 2.2f),
+ 0,
+ 1);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.setColorSpace(cs));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceAlpha8() {
+ bitmap = Bitmap.createBitmap(10, 10, Config.ALPHA_8);
+ assertNull(bitmap.getColorSpace());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> bitmap.setColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceReducedRange() {
+ ColorSpace aces = ColorSpace.get(Named.ACES);
+ bitmap = Bitmap.createBitmap(10, 10, Config.RGBA_F16, true, aces);
+ try {
+ bitmap.setColorSpace(ColorSpace.get(Named.SRGB));
+ fail("Expected IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ assertSame(aces, bitmap.getColorSpace());
+ }
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceNotReducedRange() {
+ ColorSpace extended = ColorSpace.get(Named.EXTENDED_SRGB);
+ bitmap = Bitmap.createBitmap(10, 10, Config.RGBA_F16, true, extended);
+ bitmap.setColorSpace(ColorSpace.get(Named.SRGB));
+ assertSame(bitmap.getColorSpace(), extended);
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceNotReducedRangeLinear() {
+ ColorSpace linearExtended = ColorSpace.get(Named.LINEAR_EXTENDED_SRGB);
+ bitmap = Bitmap.createBitmap(10, 10, Config.RGBA_F16, true, linearExtended);
+ bitmap.setColorSpace(ColorSpace.get(Named.LINEAR_SRGB));
+ assertSame(bitmap.getColorSpace(), linearExtended);
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testSetColorSpaceIncreasedRange() {
+ bitmap = Bitmap.createBitmap(10, 10, Config.RGBA_F16, true, ColorSpace.get(Named.DISPLAY_P3));
+ ColorSpace linearExtended = ColorSpace.get(Named.LINEAR_EXTENDED_SRGB);
+ bitmap.setColorSpace(linearExtended);
+ assertSame(bitmap.getColorSpace(), linearExtended);
+ }
+
+ @Test
+ public void testSetConfig() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+ int alloc = bitmap.getAllocationByteCount();
+
+ // test shrinking
+ bitmap.setConfig(Bitmap.Config.ALPHA_8);
+ assertEquals(bitmap.getAllocationByteCount(), alloc);
+ assertEquals(bitmap.getByteCount() * 2, alloc);
+ }
+
+ @Test
+ public void testSetConfigExpanding() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+ // test expanding
+ assertThrows(IllegalArgumentException.class, () -> bitmap.setConfig(Bitmap.Config.ARGB_8888));
+ }
+
+ @Test
+ public void testSetConfigMutable() {
+ // test mutable
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+ assertThrows(IllegalStateException.class, () -> bitmap.setConfig(Bitmap.Config.ALPHA_8));
+ }
+
+ @Test
+ public void testSetHeight() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
+ int alloc = bitmap.getAllocationByteCount();
+
+ // test shrinking
+ bitmap.setHeight(100);
+ assertEquals(bitmap.getAllocationByteCount(), alloc);
+ assertEquals(bitmap.getByteCount() * 2, alloc);
+ }
+
+ @Test
+ public void testSetHeightExpanding() {
+ // test expanding
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
+ assertThrows(IllegalArgumentException.class, () -> bitmap.setHeight(201));
+ }
+
+ @Test
+ public void testSetHeightMutable() {
+ // test mutable
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+ assertThrows(IllegalStateException.class, () -> bitmap.setHeight(1));
+ }
+
+ @Test
+ public void testSetPixelOnRecycled() {
+ int color = 0xff << 24;
+
+ bitmap.recycle();
+ assertThrows(IllegalStateException.class, () -> bitmap.setPixel(10, 16, color));
+ }
+
+ @Test
+ public void testSetPixelOnImmutable() {
+ int color = 0xff << 24;
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+
+ assertThrows(IllegalStateException.class, () -> bitmap.setPixel(10, 16, color));
+ }
+
+ @Test
+ public void testSetPixelXIsTooLarge() {
+ int color = 0xff << 24;
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+
+ // abnormal case: x bigger than the source bitmap's width
+ assertThrows(IllegalArgumentException.class, () -> bitmap.setPixel(200, 16, color));
+ }
+
+ @Test
+ public void testSetPixelYIsTooLarge() {
+ int color = 0xff << 24;
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+
+ // abnormal case: y bigger than the source bitmap's height
+ assertThrows(IllegalArgumentException.class, () -> bitmap.setPixel(10, 300, color));
+ }
+
+ @Test
+ public void testSetPixel() {
+ int color = 0xff << 24;
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565);
+
+ // normal case
+ bitmap.setPixel(10, 16, color);
+ assertEquals(color, bitmap.getPixel(10, 16));
+ }
+
+ @Test
+ public void testSetPixelsOnRecycled() {
+ int[] colors = createColors(100);
+
+ bitmap.recycle();
+ assertThrows(IllegalStateException.class, () -> bitmap.setPixels(colors, 0, 0, 0, 0, 0, 0));
+ }
+
+ @Test
+ public void testSetPixelsOnImmutable() {
+ int[] colors = createColors(100);
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+
+ assertThrows(IllegalStateException.class, () -> bitmap.setPixels(colors, 0, 0, 0, 0, 0, 0));
+ }
+
+ @Test
+ public void testSetPixelsXYNegative() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: x and/or y less than 0
+ assertThrows(
+ IllegalArgumentException.class, () -> bitmap.setPixels(colors, 0, 0, -1, -1, 200, 16));
+ }
+
+ @Test
+ public void testSetPixelsWidthHeightNegative() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: width and/or height less than 0
+ assertThrows(
+ IllegalArgumentException.class, () -> bitmap.setPixels(colors, 0, 0, 0, 0, -1, -1));
+ }
+
+ @Test
+ public void testSetPixelsXTooHigh() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: (x + width) bigger than the source bitmap's width
+ assertThrows(
+ IllegalArgumentException.class, () -> bitmap.setPixels(colors, 0, 0, 10, 10, 95, 50));
+ }
+
+ @Test
+ public void testSetPixelsYTooHigh() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: (y + height) bigger than the source bitmap's height
+ assertThrows(
+ IllegalArgumentException.class, () -> bitmap.setPixels(colors, 0, 0, 10, 10, 50, 95));
+ }
+
+ @Test
+ public void testSetPixelsStrideIllegal() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: stride less than width and bigger than -width
+ assertThrows(
+ IllegalArgumentException.class, () -> bitmap.setPixels(colors, 0, 10, 10, 10, 50, 50));
+ }
+
+ @Test
+ public void testSetPixelsOffsetNegative() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: offset less than 0
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> bitmap.setPixels(colors, -1, 50, 10, 10, 50, 50));
+ }
+
+ @Test
+ public void testSetPixelsOffsetTooBig() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: (offset + width) bigger than the length of colors
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> bitmap.setPixels(colors, 60, 50, 10, 10, 50, 50));
+ }
+
+ @Test
+ public void testSetPixelsLastScanlineNegative() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: lastScanline less than 0
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> bitmap.setPixels(colors, 10, -50, 10, 10, 50, 50));
+ }
+
+ @Test
+ public void testSetPixelsLastScanlineTooBig() {
+ int[] colors = createColors(100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+ // abnormal case: (lastScanline + width) bigger than the length of colors
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> bitmap.setPixels(colors, 10, 50, 10, 10, 50, 50));
+ }
+
+ @Test
+ public void testSetPixels() {
+ int[] colors = createColors(100 * 100);
+ bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+ bitmap.setPixels(colors, 0, 100, 0, 0, 100, 100);
+ int[] ret = new int[100 * 100];
+ bitmap.getPixels(ret, 0, 100, 0, 0, 100, 100);
+
+ for (int i = 0; i < 10000; i++) {
+ assertEquals(ret[i], colors[i]);
+ }
+ }
+
+ private void verifyPremultipliedBitmapConfig(Config config, boolean expectedPremul) {
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, config);
+ bitmap.setPremultiplied(true);
+ bitmap.setPixel(0, 0, Color.TRANSPARENT);
+ assertTrue(bitmap.isPremultiplied() == expectedPremul);
+
+ bitmap.setHasAlpha(false);
+ assertFalse(bitmap.isPremultiplied());
+ }
+
+ @Test
+ public void testSetPremultipliedSimple() {
+ verifyPremultipliedBitmapConfig(Bitmap.Config.ALPHA_8, true);
+ verifyPremultipliedBitmapConfig(Bitmap.Config.RGB_565, false);
+ verifyPremultipliedBitmapConfig(Bitmap.Config.ARGB_4444, true);
+ verifyPremultipliedBitmapConfig(Bitmap.Config.ARGB_8888, true);
+ }
+
+ @Test
+ public void testSetPremultipliedData() {
+ // with premul, will store 2,2,2,2, so it doesn't get value correct
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ bitmap.setPixel(0, 0, PREMUL_COLOR);
+ assertEquals(bitmap.getPixel(0, 0), PREMUL_ROUNDED_COLOR);
+
+ // read premultiplied value directly
+ bitmap.setPremultiplied(false);
+ assertEquals(bitmap.getPixel(0, 0), PREMUL_STORED_COLOR);
+
+ // value can now be stored/read correctly
+ bitmap.setPixel(0, 0, PREMUL_COLOR);
+ assertEquals(bitmap.getPixel(0, 0), PREMUL_COLOR);
+
+ // verify with array methods
+ int[] testArray = new int[] {PREMUL_COLOR};
+ bitmap.setPixels(testArray, 0, 1, 0, 0, 1, 1);
+ bitmap.getPixels(testArray, 0, 1, 0, 0, 1, 1);
+ assertEquals(bitmap.getPixel(0, 0), PREMUL_COLOR);
+ }
+
+ @Test
+ public void testPremultipliedCanvas() {
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ bitmap.setHasAlpha(true);
+ bitmap.setPremultiplied(false);
+ assertFalse(bitmap.isPremultiplied());
+
+ Canvas c = new Canvas();
+ assertThrows(RuntimeException.class, () -> c.drawBitmap(bitmap, 0, 0, null));
+ }
+
+ @Test
+ public void testSetWidth() {
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
+ int alloc = bitmap.getAllocationByteCount();
+
+ // test shrinking
+ bitmap.setWidth(50);
+ assertEquals(bitmap.getAllocationByteCount(), alloc);
+ assertEquals(bitmap.getByteCount() * 2, alloc);
+ }
+
+ @Test
+ public void testSetWidthExpanding() {
+ // test expanding
+ bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
+
+ assertThrows(IllegalArgumentException.class, () -> bitmap.setWidth(101));
+ }
+
+ @Test
+ public void testSetWidthMutable() {
+ // test mutable
+ bitmap = BitmapFactory.decodeResource(res, R.drawable.start, options);
+
+ assertThrows(IllegalStateException.class, () -> bitmap.setWidth(1));
+ }
+
+ @Test
+ public void testWriteToParcelRecycled() {
+ bitmap.recycle();
+
+ assertThrows(IllegalStateException.class, () -> bitmap.writeToParcel(null, 0));
+ }
+
+ @Test
+ public void testGetScaledHeight1() {
+ int dummyDensity = 5;
+ Bitmap ret = Bitmap.createBitmap(100, 200, Config.RGB_565);
+ int scaledHeight = scaleFromDensity(ret.getHeight(), ret.getDensity(), dummyDensity);
+ assertNotNull(ret);
+ assertEquals(scaledHeight, ret.getScaledHeight(dummyDensity));
+ }
+
+ @Test
+ public void testGetScaledHeight2() {
+ Bitmap ret = Bitmap.createBitmap(100, 200, Config.RGB_565);
+ DisplayMetrics metrics = RuntimeEnvironment.getApplication().getResources().getDisplayMetrics();
+ int scaledHeight = scaleFromDensity(ret.getHeight(), ret.getDensity(), metrics.densityDpi);
+ assertEquals(scaledHeight, ret.getScaledHeight(metrics));
+ }
+
+ @Test
+ public void testGetScaledHeight3() {
+ Bitmap ret = Bitmap.createBitmap(100, 200, Config.RGB_565);
+ Bitmap mMutableBitmap = Bitmap.createBitmap(100, 200, Config.ARGB_8888);
+ Canvas mCanvas = new Canvas(mMutableBitmap);
+ // set Density
+ mCanvas.setDensity(DisplayMetrics.DENSITY_HIGH);
+ int scaledHeight = scaleFromDensity(ret.getHeight(), ret.getDensity(), mCanvas.getDensity());
+ assertEquals(scaledHeight, ret.getScaledHeight(mCanvas));
+ }
+
+ @Test
+ public void testGetScaledWidth1() {
+ int dummyDensity = 5;
+ Bitmap ret = Bitmap.createBitmap(100, 200, Config.RGB_565);
+ int scaledWidth = scaleFromDensity(ret.getWidth(), ret.getDensity(), dummyDensity);
+ assertNotNull(ret);
+ assertEquals(scaledWidth, ret.getScaledWidth(dummyDensity));
+ }
+
+ @Test
+ public void testGetScaledWidth2() {
+ Bitmap ret = Bitmap.createBitmap(100, 200, Config.RGB_565);
+ DisplayMetrics metrics = RuntimeEnvironment.getApplication().getResources().getDisplayMetrics();
+ int scaledWidth = scaleFromDensity(ret.getWidth(), ret.getDensity(), metrics.densityDpi);
+ assertEquals(scaledWidth, ret.getScaledWidth(metrics));
+ }
+
+ @Test
+ public void testGetScaledWidth3() {
+ Bitmap ret = Bitmap.createBitmap(100, 200, Config.RGB_565);
+ Bitmap mMutableBitmap = Bitmap.createBitmap(100, 200, Config.ARGB_8888);
+ Canvas mCanvas = new Canvas(mMutableBitmap);
+ // set Density
+ mCanvas.setDensity(DisplayMetrics.DENSITY_HIGH);
+ int scaledWidth = scaleFromDensity(ret.getWidth(), ret.getDensity(), mCanvas.getDensity());
+ assertEquals(scaledWidth, ret.getScaledWidth(mCanvas));
+ }
+
+ @Test
+ public void testSameAs_simpleSuccess() {
+ Bitmap bitmap1 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap bitmap2 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ bitmap1.eraseColor(Color.BLACK);
+ bitmap2.eraseColor(Color.BLACK);
+ assertTrue(bitmap1.sameAs(bitmap2));
+ assertTrue(bitmap2.sameAs(bitmap1));
+ }
+
+ @Test
+ public void testSameAs_simpleFail() {
+ Bitmap bitmap1 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap bitmap2 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ bitmap1.eraseColor(Color.BLACK);
+ bitmap2.eraseColor(Color.BLACK);
+ bitmap2.setPixel(20, 10, Color.WHITE);
+ assertFalse(bitmap1.sameAs(bitmap2));
+ assertFalse(bitmap2.sameAs(bitmap1));
+ }
+
+ @Test
+ public void testSameAs_reconfigure() {
+ Bitmap bitmap1 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap bitmap2 = Bitmap.createBitmap(150, 150, Config.ARGB_8888);
+ bitmap2.reconfigure(100, 100, Config.ARGB_8888); // now same size, so should be same
+ bitmap1.eraseColor(Color.BLACK);
+ bitmap2.eraseColor(Color.BLACK);
+ assertTrue(bitmap1.sameAs(bitmap2));
+ assertTrue(bitmap2.sameAs(bitmap1));
+ }
+
+ @Test
+ public void testSameAs_config() {
+ Bitmap bitmap1 = Bitmap.createBitmap(100, 200, Config.RGB_565);
+ Bitmap bitmap2 = Bitmap.createBitmap(100, 200, Config.ARGB_8888);
+
+ // both bitmaps can represent black perfectly
+ bitmap1.eraseColor(Color.BLACK);
+ bitmap2.eraseColor(Color.BLACK);
+
+ // but not same due to config
+ assertFalse(bitmap1.sameAs(bitmap2));
+ assertFalse(bitmap2.sameAs(bitmap1));
+ }
+
+ @Test
+ public void testSameAs_width() {
+ Bitmap bitmap1 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap bitmap2 = Bitmap.createBitmap(101, 100, Config.ARGB_8888);
+ bitmap1.eraseColor(Color.BLACK);
+ bitmap2.eraseColor(Color.BLACK);
+ assertFalse(bitmap1.sameAs(bitmap2));
+ assertFalse(bitmap2.sameAs(bitmap1));
+ }
+
+ @Test
+ public void testSameAs_height() {
+ Bitmap bitmap1 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap bitmap2 = Bitmap.createBitmap(102, 100, Config.ARGB_8888);
+ bitmap1.eraseColor(Color.BLACK);
+ bitmap2.eraseColor(Color.BLACK);
+ assertFalse(bitmap1.sameAs(bitmap2));
+ assertFalse(bitmap2.sameAs(bitmap1));
+ }
+
+ @Test
+ public void testSameAs_opaque() {
+ Bitmap bitmap1 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap bitmap2 = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ bitmap1.eraseColor(Color.BLACK);
+ bitmap2.eraseColor(Color.BLACK);
+ bitmap1.setHasAlpha(true);
+ bitmap2.setHasAlpha(false);
+ assertFalse(bitmap1.sameAs(bitmap2));
+ assertFalse(bitmap2.sameAs(bitmap1));
+ }
+
+ @Test
+ public void testSameAs_hardware() {
+ Bitmap bitmap1 = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ Bitmap bitmap2 = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ Bitmap bitmap3 = BitmapFactory.decodeResource(res, R.drawable.robot);
+ Bitmap bitmap4 = BitmapFactory.decodeResource(res, R.drawable.start, HARDWARE_OPTIONS);
+ assertTrue(bitmap1.sameAs(bitmap2));
+ assertTrue(bitmap2.sameAs(bitmap1));
+ assertFalse(bitmap1.sameAs(bitmap3));
+ assertFalse(bitmap1.sameAs(bitmap4));
+ }
+
+ @Test
+ public void testHardwareSetWidth() {
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ assertThrows(IllegalStateException.class, () -> bitmap.setWidth(30));
+ }
+
+ @Test
+ public void testHardwareSetHeight() {
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ assertThrows(IllegalStateException.class, () -> bitmap.setHeight(30));
+ }
+
+ @Test
+ public void testHardwareSetConfig() {
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ assertThrows(IllegalStateException.class, () -> bitmap.setConfig(Config.ARGB_8888));
+ }
+
+ @Test
+ public void testHardwareReconfigure() {
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ assertThrows(IllegalStateException.class, () -> bitmap.reconfigure(30, 30, Config.ARGB_8888));
+ }
+
+ @Test
+ public void testHardwareSetPixels() {
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ assertThrows(
+ IllegalStateException.class, () -> bitmap.setPixels(new int[10], 0, 1, 0, 0, 1, 1));
+ }
+
+ @Test
+ public void testHardwareSetPixel() {
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ assertThrows(IllegalStateException.class, () -> bitmap.setPixel(1, 1, 0));
+ }
+
+ @Test
+ public void testHardwareEraseColor() {
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ assertThrows(IllegalStateException.class, () -> bitmap.eraseColor(0));
+ }
+
+ @org.robolectric.annotation.Config(minSdk = Q)
+ @Test
+ public void testHardwareEraseColorLong() {
+ Bitmap bitmap = BitmapFactory.decodeResource(res, R.drawable.robot, HARDWARE_OPTIONS);
+ assertThrows(IllegalStateException.class, () -> bitmap.eraseColor(Color.pack(0)));
+ }
+
+ @Test
+ public void testUseMetadataAfterRecycle() {
+ Bitmap bitmap = Bitmap.createBitmap(10, 20, Config.RGB_565);
+ bitmap.recycle();
+ assertEquals(10, bitmap.getWidth());
+ assertEquals(20, bitmap.getHeight());
+ assertEquals(Config.RGB_565, bitmap.getConfig());
+ }
+
+ @Test
+ @Ignore("TODO(b/hoisie): re-enable when HW bitmaps are better supported")
+ public void testCreateScaledFromHWInStrictMode() {
+ strictModeTest(
+ () -> {
+ Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap hwBitmap = bitmap.copy(Config.HARDWARE, false);
+ Bitmap.createScaledBitmap(hwBitmap, 200, 200, false);
+ });
+ }
+
+ @Test
+ public void testCompressInStrictMode() {
+ strictModeTest(
+ () -> {
+ Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ bitmap.compress(CompressFormat.JPEG, 90, new ByteArrayOutputStream());
+ });
+ }
+
+ @Test
+ public void legacyShadowAPIs_throwException() {
+ Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ ShadowBitmap shadowBitmap = Shadow.extract(bitmap);
+ assertThrows(UnsupportedOperationException.class, () -> shadowBitmap.setDescription("hello"));
+ }
+
+ @Ignore("TODO(b/hoisie): re-enable when HW bitmaps are better supported")
+ @Test
+ public void testParcelHWInStrictMode() {
+ strictModeTest(
+ () -> {
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap hwBitmap = bitmap.copy(Config.HARDWARE, false);
+ hwBitmap.writeToParcel(Parcel.obtain(), 0);
+ });
+ }
+
+ @Test
+ public void getCreatedFromResId() {
+ assertThat(((ShadowNativeBitmap) Shadow.extract(bitmap)).getCreatedFromResId())
+ .isEqualTo(R.drawable.start);
+ }
+
+ @Test
+ public void testWriteToParcel() {
+ Parcel p = Parcel.obtain();
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ bitmap.eraseColor(Color.GREEN);
+ bitmap.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ Bitmap fromParcel = Bitmap.CREATOR.createFromParcel(p);
+ assertTrue(bitmap.sameAs(fromParcel));
+ assertThat(fromParcel.isMutable()).isTrue();
+ p.recycle();
+ }
+
+ @Test
+ public void testWriteImmutableToParcel() {
+ Parcel p = Parcel.obtain();
+ bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ bitmap.eraseColor(Color.GREEN);
+ Bitmap immutable = bitmap.copy(Config.ARGB_8888, /*isMutable=*/ false);
+ assertThat(immutable.isMutable()).isFalse();
+ immutable.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ Bitmap fromParcel = Bitmap.CREATOR.createFromParcel(p);
+ assertTrue(immutable.sameAs(fromParcel));
+ assertThat(fromParcel.isMutable()).isFalse();
+ p.recycle();
+ }
+
+ @Test
+ public void createBitmap_colorSpace_customColorSpace() {
+ Bitmap bitmap =
+ Bitmap.createBitmap(
+ 100, 100, Bitmap.Config.ARGB_8888, true, ColorSpace.get(ColorSpace.Named.ADOBE_RGB));
+
+ assertThat(bitmap.getColorSpace()).isEqualTo(ColorSpace.get(ColorSpace.Named.ADOBE_RGB));
+ }
+
+ @Test
+ public void compress_thenDecodeStream_sameAs() {
+ Bitmap bitmap = Bitmap.createBitmap(/* width= */ 10, /* height= */ 10, Bitmap.Config.ARGB_8888);
+ ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+ bitmap.compress(CompressFormat.PNG, /* quality= */ 100, outStream);
+ byte[] outBytes = outStream.toByteArray();
+ ByteArrayInputStream inStream = new ByteArrayInputStream(outBytes);
+ BitmapFactory.Options options = new Options();
+ Bitmap bitmap2 = BitmapFactory.decodeStream(inStream, null, options);
+ assertThat(bitmap.sameAs(bitmap2)).isTrue();
+ }
+
+ @Test
+ public void parcelRoundTripWithoutColorSpace_isSuccessful() {
+ // Importantly, ALPHA_8 doesn't have an associated color space.
+ Bitmap orig = Bitmap.createBitmap(/* width= */ 314, /* height= */ 159, Bitmap.Config.ALPHA_8);
+
+ Parcel parcel = Parcel.obtain();
+ parcel.writeParcelable(orig, /* parcelableFlags= */ 0);
+ parcel.setDataPosition(0);
+ Bitmap copy = parcel.readParcelable(Bitmap.class.getClassLoader());
+
+ assertThat(copy.sameAs(orig)).isTrue();
+ }
+
+ private void strictModeTest(Runnable runnable) {
+ StrictMode.ThreadPolicy originalPolicy = StrictMode.getThreadPolicy();
+ StrictMode.setThreadPolicy(
+ new StrictMode.ThreadPolicy.Builder().detectCustomSlowCalls().penaltyDeath().build());
+ try {
+ runnable.run();
+ fail("Shouldn't reach it");
+ } catch (RuntimeException expected) {
+ // expect to receive StrictModeViolation
+ } finally {
+ StrictMode.setThreadPolicy(originalPolicy);
+ }
+ }
+
+ static final int ANDROID_BITMAP_FORMAT_RGBA_8888 = 1;
+
+ private static int scaleFromDensity(int size, int sdensity, int tdensity) {
+ if (sdensity == Bitmap.DENSITY_NONE || sdensity == tdensity) {
+ return size;
+ }
+
+ // Scale by tdensity / sdensity, rounding up.
+ return ((size * tdensity) + (sdensity >> 1)) / sdensity;
+ }
+
+ private static int[] createColors(int size) {
+ int[] colors = new int[size];
+
+ for (int i = 0; i < size; i++) {
+ colors[i] = (0xFF << 24) | (i << 16) | (i << 8) | i;
+ }
+
+ return colors;
+ }
+
+ private static BitmapFactory.Options createHardwareBitmapOptions() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.HARDWARE;
+ return options;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBlendModeColorFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBlendModeColorFilterTest.java
new file mode 100644
index 000000000..e427e9c8d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBlendModeColorFilterTest.java
@@ -0,0 +1,96 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.BlendMode;
+import android.graphics.BlendModeColorFilter;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Point;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Q) // Added in API 29
+public class ShadowNativeBlendModeColorFilterTest {
+ private static final int TOLERANCE = 5;
+
+ private static final int TEST_WIDTH = 90;
+ private static final int TEST_HEIGHT = 90;
+ private static final int LEFT_X = TEST_WIDTH / 4;
+ private static final int RIGHT_X = TEST_WIDTH * 3 / 4;
+ private static final int TOP_Y = TEST_HEIGHT / 4;
+ private static final int BOTTOM_Y = TEST_HEIGHT * 3 / 4;
+
+ private static final int FILTER_COLOR = Color.argb(0x80, 0, 0xFF, 0);
+
+ private static final Point[] SAMPLE_POINTS = {
+ new Point(LEFT_X, TOP_Y), new Point(LEFT_X, BOTTOM_Y), new Point(RIGHT_X, BOTTOM_Y)
+ };
+
+ private void testBlendModeColorFilter(int filterColor, BlendMode mode, int[] colors) {
+ // The left side will be red.
+ final Bitmap b1 = Bitmap.createBitmap(TEST_WIDTH / 2, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+ b1.eraseColor(Color.RED);
+ // The bottom will be blue.
+ final Bitmap b2 = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT / 2, Bitmap.Config.ARGB_8888);
+ b2.eraseColor(Color.BLUE);
+
+ // This will be the final image, which is the blended combination of the above two bitmaps
+ // on an otherwise white bitmap.
+ final Bitmap b3 = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+ b3.eraseColor(Color.WHITE);
+
+ Canvas canvas = new Canvas(b3);
+
+ canvas.drawColor(Color.WHITE);
+
+ BlendModeColorFilter filter = new BlendModeColorFilter(filterColor, mode);
+ Paint p = new Paint();
+ canvas.drawBitmap(b1, 0, 0, p);
+ p.setColorFilter(filter);
+ canvas.drawBitmap(b2, 0, TEST_HEIGHT / 2, p);
+
+ for (int i = 0; i < SAMPLE_POINTS.length; i++) {
+ Point point = SAMPLE_POINTS[i];
+ assertThat(Integer.toHexString(b3.getPixel(point.x, point.y)))
+ .isEqualTo(Integer.toHexString(colors[i]));
+ }
+ }
+
+ @Test
+ public void testBlendModeColorFilter_SRC() {
+ testBlendModeColorFilter(
+ FILTER_COLOR, BlendMode.SRC, new int[] {Color.RED, 0xFF7F8000, 0xFF7FFF7f});
+ }
+
+ @Test
+ public void testBlendModeColorFilter_DST() {
+ testBlendModeColorFilter(
+ FILTER_COLOR, BlendMode.DST, new int[] {Color.RED, Color.BLUE, Color.BLUE});
+ }
+
+ @Test
+ public void testBlendModeColorFilter_SCREEN() {
+ testBlendModeColorFilter(
+ Color.GREEN, BlendMode.SCREEN, new int[] {Color.RED, Color.CYAN, Color.CYAN});
+ }
+
+ @Test
+ public void testBlendModeColorFilterGetMode() {
+ BlendModeColorFilter filter = new BlendModeColorFilter(Color.CYAN, BlendMode.SOFT_LIGHT);
+ assertEquals(BlendMode.SOFT_LIGHT, filter.getMode());
+ }
+
+ @Test
+ public void testBlendModeColorFilterGetColor() {
+ BlendModeColorFilter filter = new BlendModeColorFilter(Color.MAGENTA, BlendMode.HARD_LIGHT);
+ assertEquals(Color.MAGENTA, filter.getColor());
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBlurMaskFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBlurMaskFilterTest.java
new file mode 100644
index 000000000..59ce06941
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeBlurMaskFilterTest.java
@@ -0,0 +1,69 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.BlurMaskFilter.Blur;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeBlurMaskFilterTest {
+ private static final int OFFSET = 10;
+ private static final int RADIUS = 5;
+ private static final int CHECK_RADIUS = 8;
+ private static final int BITMAP_WIDTH = 100;
+ private static final int BITMAP_HEIGHT = 100;
+ private static final int CENTER = BITMAP_HEIGHT / 2;
+
+ @Test
+ public void testBlurMaskFilter() {
+ BlurMaskFilter filter = new BlurMaskFilter(RADIUS, Blur.NORMAL);
+ Paint paint = new Paint();
+ paint.setMaskFilter(filter);
+ paint.setColor(Color.RED);
+ Bitmap b = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
+ b.eraseColor(Color.TRANSPARENT);
+ Canvas canvas = new Canvas(b);
+ canvas.drawRect(CENTER - OFFSET, CENTER - OFFSET, CENTER + OFFSET, CENTER + OFFSET, paint);
+ for (int x = 0; x < CENTER; x++) {
+ for (int y = 0; y < CENTER; y++) {
+ if (x < CENTER - OFFSET - CHECK_RADIUS || y < CENTER - OFFSET - CHECK_RADIUS) {
+ // check that color didn't bleed (much) beyond radius
+ verifyQuadrants(Color.TRANSPARENT, b, x, y, 5);
+ } else if (x > CENTER - OFFSET + RADIUS && y > CENTER - OFFSET + RADIUS) {
+ // check that color didn't wash out (much) in the center
+ verifyQuadrants(Color.RED, b, x, y, 8);
+ } else if (x > CENTER - OFFSET - RADIUS && y > CENTER - OFFSET - RADIUS) {
+ // check blur zone, color should remain, alpha varies
+ verifyQuadrants(Color.RED, b, x, y, 255);
+ }
+ }
+ }
+ }
+
+ private void verifyQuadrants(int color, Bitmap bitmap, int x, int y, int alphaTolerance) {
+ int right = bitmap.getWidth() - 1;
+ int bottom = bitmap.getHeight() - 1;
+
+ verifyColor(color, bitmap.getPixel(x, y), alphaTolerance);
+ verifyColor(color, bitmap.getPixel(right - x, y), alphaTolerance);
+ verifyColor(color, bitmap.getPixel(x, bottom - y), alphaTolerance);
+ verifyColor(color, bitmap.getPixel(right - x, bottom - y), alphaTolerance);
+ }
+
+ private void verifyColor(int expected, int actual, int alphaTolerance) {
+ assertEquals(Color.red(expected), Color.red(actual));
+ assertEquals(Color.green(expected), Color.green(actual));
+ assertEquals(Color.blue(expected), Color.blue(actual));
+ assertEquals(Color.alpha(expected), Color.alpha(actual), alphaTolerance);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeCanvasTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeCanvasTest.java
new file mode 100644
index 000000000..f590cf812
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeCanvasTest.java
@@ -0,0 +1,77 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeCanvasTest {
+ @Test
+ public void testDrawColor() {
+ Bitmap bm = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bm);
+ canvas.drawColor(Color.BLUE);
+ assertThat(bm.getPixel(0, 0)).isEqualTo(Color.BLUE);
+ }
+
+ @Test
+ public void testDrawPaint() {
+ Bitmap bm = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bm);
+
+ Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ p.setColor(Color.BLUE);
+ canvas.drawRect(new Rect(0, 0, 100, 100), p);
+ assertThat(bm.getPixel(0, 0)).isEqualTo(Color.BLUE);
+ }
+
+ @Test
+ public void testSetBitmap() {
+ Bitmap bm = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas();
+ canvas.setBitmap(bm);
+
+ Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ p.setColor(Color.BLUE);
+ canvas.drawRect(new Rect(0, 0, 100, 100), p);
+ assertThat(bm.getPixel(0, 0)).isEqualTo(Color.BLUE);
+ }
+
+ @Test
+ public void testDrawTextRunChars() {
+ Bitmap bm = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas();
+ canvas.setBitmap(bm);
+ Paint paint = new Paint();
+ paint.setColor(Color.WHITE);
+ paint.setStyle(Style.FILL);
+ canvas.drawPaint(paint);
+ paint.setColor(Color.BLACK);
+ canvas.drawTextRun(new char[] {'h', 'e', 'l', 'l', 'o'}, 0, 5, 0, 5, 30, 30, false, paint);
+ assertThat(isAllWhite(bm)).isFalse(); // check *something* was drawn
+ }
+
+ private boolean isAllWhite(Bitmap bitmap) {
+ for (int i = 0; i < bitmap.getWidth(); i++) {
+ for (int j = 0; j < bitmap.getHeight(); j++) {
+ if (bitmap.getPixel(i, j) != Color.WHITE) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorFilterTest.java
new file mode 100644
index 000000000..646e373e1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorFilterTest.java
@@ -0,0 +1,82 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.os.Build.VERSION;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.base.MoreObjects;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers.ColorVerifier;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeColorFilterTest {
+ @Test
+ public void testColorMatrix() throws Exception {
+ int width = 90;
+ int height = 90;
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.WHITE);
+
+ // White, full opacity
+ assertThat(Integer.toHexString(bitmap.getPixel(0, 0))).isEqualTo("ffffffff");
+
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setTypeface(Typeface.DEFAULT);
+ paint.setAntiAlias(true);
+ paint.setColorFilter(
+ new ColorMatrixColorFilter(
+ new ColorMatrix(
+ new float[] {
+ -1, 0, 0, 0, 255,
+ 0, -1, 0, 0, 255,
+ 0, 0, -1, 0, 255,
+ 0, 0, 0, 1, 0
+ })));
+
+ canvas.drawBitmap(bitmap, 0, 0, paint);
+
+ // Black, full opacity
+ assertThat(Integer.toHexString(bitmap.getPixel(0, 0))).isEqualTo("ff000000");
+
+ ColorVerifier colorVerifier = new ColorVerifier(Color.BLACK);
+
+ // Check every pixel is black.
+ if (!colorVerifier.verify(bitmap)) {
+ Bitmap diff = colorVerifier.getDifferenceBitmap();
+ takeScreenshot(diff, "diff.png");
+ fail(
+ "Bitmap was not the correct color. See diff.png for difference (red shows different"
+ + " pixels).");
+ }
+ }
+
+ private void takeScreenshot(Bitmap bitmap, String name) throws IOException {
+ // TEST_UNDECLARED_OUTPUTS_DIR is better in a Bazel environment because the files show up
+ // in test artifacts.
+ String outputDir =
+ MoreObjects.firstNonNull(
+ System.getenv("TEST_UNDECLARED_OUTPUTS_DIR"), System.getProperty("java.io.tmpdir"));
+ File f = new File(outputDir, "sdk" + VERSION.SDK_INT + "_" + name);
+ f.createNewFile();
+ f.deleteOnExit();
+ bitmap.compress(CompressFormat.PNG, 100, new FileOutputStream(f));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorMatrixColorFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorMatrixColorFilterTest.java
new file mode 100644
index 000000000..4b080e993
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorMatrixColorFilterTest.java
@@ -0,0 +1,114 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeColorMatrixColorFilterTest {
+
+ @Test
+ public void testColorMatrixColorFilter() {
+ ColorMatrixColorFilter filter;
+
+ ColorMatrix cm = new ColorMatrix();
+ float[] blueToCyan =
+ new float[] {
+ 1f, 0f, 0f, 0f, 0f,
+ 0f, 1f, 1f, 0f, 0f,
+ 0f, 0f, 1f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f
+ };
+ cm.set(blueToCyan);
+ filter = new ColorMatrixColorFilter(cm);
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setColor(Color.BLUE);
+ paint.setColorFilter(filter);
+ paint.setAntiAlias(false);
+ canvas.drawPoint(0, 0, paint);
+ ColorUtils.verifyColor(Color.CYAN, bitmap.getPixel(0, 0));
+
+ paint.setColor(Color.GREEN);
+ canvas.drawPoint(0, 0, paint);
+ ColorUtils.verifyColor(Color.GREEN, bitmap.getPixel(0, 0));
+
+ paint.setColor(Color.RED);
+ canvas.drawPoint(0, 0, paint);
+ ColorUtils.verifyColor(Color.RED, bitmap.getPixel(0, 0));
+
+ // color components are clipped, not scaled
+ paint.setColor(Color.MAGENTA);
+ canvas.drawPoint(0, 0, paint);
+ ColorUtils.verifyColor(Color.WHITE, bitmap.getPixel(0, 0));
+
+ float[] transparentRedAddBlue =
+ new float[] {
+ 1f, 0f, 0f, 0f, 0f,
+ 0f, 1f, 0f, 0f, 0f,
+ 0f, 0f, 1f, 0f, 64f,
+ -0.5f, 0f, 0f, 1f, 0f
+ };
+ filter = new ColorMatrixColorFilter(transparentRedAddBlue);
+ paint.setColorFilter(filter);
+ paint.setColor(Color.RED);
+ bitmap.eraseColor(Color.TRANSPARENT);
+ canvas.drawPoint(0, 0, paint);
+ // the bitmap stores the result in premul colors and we read out an
+ // unpremultiplied result, which causes us to need a bigger tolerance in
+ // this case (due to the fact that scaling by 1/255 is not exact).
+ ColorUtils.verifyColor(Color.argb(128, 255, 0, 64), bitmap.getPixel(0, 0), 2);
+ paint.setColor(Color.CYAN);
+ canvas.drawPoint(0, 0, paint);
+ // blue gets clipped
+ ColorUtils.verifyColor(Color.CYAN, bitmap.getPixel(0, 0));
+
+ // change array to filter out green
+ assertEquals(1f, transparentRedAddBlue[6], 0.0f);
+ transparentRedAddBlue[6] = 0f;
+ // changing the array has no effect
+ canvas.drawPoint(0, 0, paint);
+ ColorUtils.verifyColor(Color.CYAN, bitmap.getPixel(0, 0));
+ // create a new filter with the changed matrix
+ paint.setColorFilter(new ColorMatrixColorFilter(transparentRedAddBlue));
+ canvas.drawPoint(0, 0, paint);
+ ColorUtils.verifyColor(Color.BLUE, bitmap.getPixel(0, 0));
+ }
+
+ @Test
+ public void testGetColorMatrix() {
+ ColorMatrixColorFilter filter = new ColorMatrixColorFilter(new ColorMatrix());
+ ColorMatrix getMatrix = new ColorMatrix();
+
+ filter.getColorMatrix(getMatrix);
+ assertEquals(new ColorMatrix(), getMatrix);
+
+ ColorMatrix scaleTranslate =
+ new ColorMatrix(
+ new float[] {
+ 1, 0, 0, 0, 8,
+ 0, 2, 0, 0, 7,
+ 0, 0, 3, 0, 6,
+ 0, 0, 0, 4, 5
+ });
+
+ filter = new ColorMatrixColorFilter(scaleTranslate);
+ filter.getColorMatrix(getMatrix);
+ assertEquals(scaleTranslate, getMatrix);
+ assertArrayEquals(scaleTranslate.getArray(), getMatrix.getArray(), 0);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorSpaceTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorSpaceTest.java
new file mode 100644
index 000000000..50fa84a88
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorSpaceTest.java
@@ -0,0 +1,912 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics;
+
+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.TIRAMISU;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.graphics.ColorSpace;
+import java.util.Arrays;
+import java.util.function.DoubleUnaryOperator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowNativeColorSpaceTest {
+ // Column-major RGB->XYZ transform matrix for the sRGB color space
+ private static final float[] SRGB_TO_XYZ = {
+ 0.412391f, 0.212639f, 0.019331f,
+ 0.357584f, 0.715169f, 0.119195f,
+ 0.180481f, 0.072192f, 0.950532f
+ };
+ // Column-major XYZ->RGB transform matrix for the sRGB color space
+ private static final float[] XYZ_TO_SRGB = {
+ 3.240970f, -0.969244f, 0.055630f,
+ -1.537383f, 1.875968f, -0.203977f,
+ -0.498611f, 0.041555f, 1.056971f
+ };
+
+ // Column-major RGB->XYZ transform matrix for the sRGB color space and a D50 white point
+ private static final float[] SRGB_TO_XYZ_D50 = {
+ 0.4360747f, 0.2225045f, 0.0139322f,
+ 0.3850649f, 0.7168786f, 0.0971045f,
+ 0.1430804f, 0.0606169f, 0.7141733f
+ };
+
+ private static final float[] rGBPRIMARIESXyY = {0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f};
+ private static final float[] sRGBWHITEPOINTXyY = {0.3127f, 0.3290f};
+
+ private static final float[] SRGB_PRIMARIES_XYZ = {
+ 1.939394f, 1.000000f, 0.090909f,
+ 0.500000f, 1.000000f, 0.166667f,
+ 2.500000f, 1.000000f, 13.166667f
+ };
+ private static final float[] SRGB_WHITE_POINT_XYZ = {0.950456f, 1.000f, 1.089058f};
+
+ private static final DoubleUnaryOperator identity = DoubleUnaryOperator.identity();
+
+ @Test
+ public void testNamedColorSpaces() {
+ for (ColorSpace.Named named : ColorSpace.Named.values()) {
+ ColorSpace colorSpace = ColorSpace.get(named);
+ assertNotNull(colorSpace.getName());
+ assertNotNull(colorSpace);
+ assertEquals(named.ordinal(), colorSpace.getId());
+ assertTrue(colorSpace.getComponentCount() >= 1);
+ assertTrue(colorSpace.getComponentCount() <= 4);
+ }
+ }
+
+ @Test
+ public void testNullName() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new ColorSpace.Rgb(null, new float[6], new float[2], identity, identity, 0.0f, 1.0f));
+ }
+
+ @Test
+ public void testEmptyName() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new ColorSpace.Rgb("", new float[6], new float[2], identity, identity, 0.0f, 1.0f));
+ }
+
+ @Test
+ public void testName() {
+ ColorSpace.Rgb cs =
+ new ColorSpace.Rgb("Test", new float[6], new float[2], identity, identity, 0.0f, 1.0f);
+ assertEquals("Test", cs.getName());
+ }
+
+ @Test
+ public void testPrimariesLength() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new ColorSpace.Rgb("Test", new float[7], new float[2], identity, identity, 0.0f, 1.0f));
+ }
+
+ @Test
+ public void testWhitePointLength() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new ColorSpace.Rgb("Test", new float[6], new float[1], identity, identity, 0.0f, 1.0f));
+ }
+
+ @Test
+ public void testNullOETF() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new ColorSpace.Rgb("Test", new float[6], new float[2], null, identity, 0.0f, 1.0f));
+ }
+
+ @Test
+ public void testOETF() {
+ DoubleUnaryOperator op = Math::sqrt;
+ ColorSpace.Rgb cs =
+ new ColorSpace.Rgb("Test", new float[6], new float[2], op, identity, 0.0f, 1.0f);
+ assertEquals(0.5, cs.getOetf().applyAsDouble(0.25), 1e-5);
+ }
+
+ @Test
+ public void testNullEOTF() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new ColorSpace.Rgb("Test", new float[6], new float[2], identity, null, 0.0f, 1.0f));
+ }
+
+ @Test
+ public void testEOTF() {
+ DoubleUnaryOperator op = x -> x * x;
+ ColorSpace.Rgb cs =
+ new ColorSpace.Rgb("Test", new float[6], new float[2], identity, op, 0.0f, 1.0f);
+ assertEquals(0.0625, cs.getEotf().applyAsDouble(0.25), 1e-5);
+ }
+
+ @Test
+ public void testInvalidRange() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new ColorSpace.Rgb("Test", new float[6], new float[2], identity, identity, 2.0f, 1.0f));
+ }
+
+ @Test
+ public void testRanges() {
+ ColorSpace cs = ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float m1 = cs.getMinValue(0);
+ float m2 = cs.getMinValue(1);
+ float m3 = cs.getMinValue(2);
+
+ assertEquals(0.0f, m1, 1e-9f);
+ assertEquals(0.0f, m2, 1e-9f);
+ assertEquals(0.0f, m3, 1e-9f);
+
+ m1 = cs.getMaxValue(0);
+ m2 = cs.getMaxValue(1);
+ m3 = cs.getMaxValue(2);
+
+ assertEquals(1.0f, m1, 0);
+ assertEquals(1.0f, m2, 0);
+ assertEquals(1.0f, m3, 0);
+
+ cs = ColorSpace.get(ColorSpace.Named.CIE_LAB);
+
+ m1 = cs.getMinValue(0);
+ m2 = cs.getMinValue(1);
+ m3 = cs.getMinValue(2);
+
+ assertEquals(0.0f, m1, 1e-9f);
+ assertEquals(-128.0f, m2, 0);
+ assertEquals(-128.0f, m3, 0);
+
+ m1 = cs.getMaxValue(0);
+ m2 = cs.getMaxValue(1);
+ m3 = cs.getMaxValue(2);
+
+ assertEquals(100.0f, m1, 0);
+ assertEquals(128.0f, m2, 0);
+ assertEquals(128.0f, m3, 0);
+
+ cs = ColorSpace.get(ColorSpace.Named.CIE_XYZ);
+
+ m1 = cs.getMinValue(0);
+ m2 = cs.getMinValue(1);
+ m3 = cs.getMinValue(2);
+
+ assertEquals(-2.0f, m1, 0);
+ assertEquals(-2.0f, m2, 0);
+ assertEquals(-2.0f, m3, 0);
+
+ m1 = cs.getMaxValue(0);
+ m2 = cs.getMaxValue(1);
+ m3 = cs.getMaxValue(2);
+
+ assertEquals(2.0f, m1, 0);
+ assertEquals(2.0f, m2, 0);
+ assertEquals(2.0f, m3, 0);
+ }
+
+ @Test
+ public void testMat3x3() {
+ ColorSpace.Rgb cs = new ColorSpace.Rgb("Test", SRGB_TO_XYZ, identity, identity);
+
+ float[] rgbToXYZ = cs.getTransform();
+ for (int i = 0; i < 9; i++) {
+ assertEquals(SRGB_TO_XYZ[i], rgbToXYZ[i], 1e-5f);
+ }
+ }
+
+ @Test
+ public void testMat3x3Inverse() {
+ ColorSpace.Rgb cs = new ColorSpace.Rgb("Test", SRGB_TO_XYZ, identity, identity);
+
+ float[] xyzToRGB = cs.getInverseTransform();
+ for (int i = 0; i < 9; i++) {
+ assertEquals(XYZ_TO_SRGB[i], xyzToRGB[i], 1e-5f);
+ }
+ }
+
+ @Test
+ public void testMat3x3Primaries() {
+ ColorSpace.Rgb cs = new ColorSpace.Rgb("Test", SRGB_TO_XYZ, identity, identity);
+
+ float[] primaries = cs.getPrimaries();
+
+ assertNotNull(primaries);
+ assertEquals(6, primaries.length);
+
+ assertEquals(rGBPRIMARIESXyY[0], primaries[0], 1e-5f);
+ assertEquals(rGBPRIMARIESXyY[1], primaries[1], 1e-5f);
+ assertEquals(rGBPRIMARIESXyY[2], primaries[2], 1e-5f);
+ assertEquals(rGBPRIMARIESXyY[3], primaries[3], 1e-5f);
+ assertEquals(rGBPRIMARIESXyY[4], primaries[4], 1e-5f);
+ assertEquals(rGBPRIMARIESXyY[5], primaries[5], 1e-5f);
+ }
+
+ @Test
+ public void testMat3x3WhitePoint() {
+ ColorSpace.Rgb cs = new ColorSpace.Rgb("Test", SRGB_TO_XYZ, identity, identity);
+
+ float[] whitePoint = cs.getWhitePoint();
+
+ assertNotNull(whitePoint);
+ assertEquals(2, whitePoint.length);
+
+ assertEquals(sRGBWHITEPOINTXyY[0], whitePoint[0], 1e-5f);
+ assertEquals(sRGBWHITEPOINTXyY[1], whitePoint[1], 1e-5f);
+ }
+
+ @Test
+ public void testXYZFromPrimaries_xyY() {
+ ColorSpace.Rgb cs =
+ new ColorSpace.Rgb(
+ "Test", rGBPRIMARIESXyY, sRGBWHITEPOINTXyY, identity, identity, 0.0f, 1.0f);
+
+ float[] rgbToXYZ = cs.getTransform();
+ for (int i = 0; i < 9; i++) {
+ assertEquals(SRGB_TO_XYZ[i], rgbToXYZ[i], 1e-5f);
+ }
+
+ float[] xyzToRGB = cs.getInverseTransform();
+ for (int i = 0; i < 9; i++) {
+ assertEquals(XYZ_TO_SRGB[i], xyzToRGB[i], 1e-5f);
+ }
+ }
+
+ @Test
+ public void testXYZFromPrimaries_xYZ() {
+ ColorSpace.Rgb cs =
+ new ColorSpace.Rgb(
+ "Test", SRGB_PRIMARIES_XYZ, SRGB_WHITE_POINT_XYZ, identity, identity, 0.0f, 1.0f);
+
+ float[] primaries = cs.getPrimaries();
+
+ assertNotNull(primaries);
+ assertEquals(6, primaries.length);
+
+ // SRGB_PRIMARIES_xyY only has 1e-3 of precision, match it
+ assertEquals(rGBPRIMARIESXyY[0], primaries[0], 1e-3f);
+ assertEquals(rGBPRIMARIESXyY[1], primaries[1], 1e-3f);
+ assertEquals(rGBPRIMARIESXyY[2], primaries[2], 1e-3f);
+ assertEquals(rGBPRIMARIESXyY[3], primaries[3], 1e-3f);
+ assertEquals(rGBPRIMARIESXyY[4], primaries[4], 1e-3f);
+ assertEquals(rGBPRIMARIESXyY[5], primaries[5], 1e-3f);
+
+ float[] whitePoint = cs.getWhitePoint();
+
+ assertNotNull(whitePoint);
+ assertEquals(2, whitePoint.length);
+
+ // SRGB_WHITE_POINT_xyY only has 1e-3 of precision, match it
+ assertEquals(sRGBWHITEPOINTXyY[0], whitePoint[0], 1e-3f);
+ assertEquals(sRGBWHITEPOINTXyY[1], whitePoint[1], 1e-3f);
+
+ float[] rgbToXYZ = cs.getTransform();
+ for (int i = 0; i < 9; i++) {
+ assertEquals(SRGB_TO_XYZ[i], rgbToXYZ[i], 1e-5f);
+ }
+
+ float[] xyzToRGB = cs.getInverseTransform();
+ for (int i = 0; i < 9; i++) {
+ assertEquals(XYZ_TO_SRGB[i], xyzToRGB[i], 1e-5f);
+ }
+ }
+
+ @Test
+ public void testGetComponentCount() {
+ assertEquals(3, ColorSpace.get(ColorSpace.Named.SRGB).getComponentCount());
+ assertEquals(3, ColorSpace.get(ColorSpace.Named.LINEAR_SRGB).getComponentCount());
+ assertEquals(3, ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB).getComponentCount());
+ assertEquals(3, ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB).getComponentCount());
+ assertEquals(3, ColorSpace.get(ColorSpace.Named.DISPLAY_P3).getComponentCount());
+ assertEquals(3, ColorSpace.get(ColorSpace.Named.CIE_LAB).getComponentCount());
+ assertEquals(3, ColorSpace.get(ColorSpace.Named.CIE_XYZ).getComponentCount());
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testIsSRGB() {
+ for (ColorSpace.Named e : ColorSpace.Named.values()) {
+ ColorSpace colorSpace = ColorSpace.get(e);
+ if (e == ColorSpace.Named.SRGB) {
+ assertTrue(colorSpace.isSrgb());
+ } else {
+ assertFalse("Incorrectly treating " + colorSpace + " as SRGB!", colorSpace.isSrgb());
+ }
+ }
+
+ ColorSpace.Rgb cs =
+ new ColorSpace.Rgb(
+ "Almost sRGB", SRGB_TO_XYZ, x -> Math.pow(x, 1.0f / 2.2f), x -> Math.pow(x, 2.2f));
+ assertFalse(cs.isSrgb());
+ }
+
+ @Test
+ @Config(minSdk = O, maxSdk = P)
+ public void testIsSRGB_o_p() {
+ assertTrue(ColorSpace.get(ColorSpace.Named.SRGB).isSrgb());
+ assertFalse(ColorSpace.get(ColorSpace.Named.LINEAR_SRGB).isSrgb());
+ assertFalse(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB).isSrgb());
+ assertFalse(ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB).isSrgb());
+ assertFalse(ColorSpace.get(ColorSpace.Named.DISPLAY_P3).isSrgb());
+ assertFalse(ColorSpace.get(ColorSpace.Named.CIE_LAB).isSrgb());
+ assertFalse(ColorSpace.get(ColorSpace.Named.CIE_XYZ).isSrgb());
+
+ ColorSpace.Rgb cs =
+ new ColorSpace.Rgb(
+ "My sRGB", SRGB_TO_XYZ, x -> Math.pow(x, 1.0f / 2.2f), x -> Math.pow(x, 2.2f));
+ assertTrue(cs.isSrgb());
+ }
+
+ @Test
+ public void testIsWideGamut() {
+ assertFalse(ColorSpace.get(ColorSpace.Named.SRGB).isWideGamut());
+ assertFalse(ColorSpace.get(ColorSpace.Named.BT709).isWideGamut());
+ assertTrue(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB).isWideGamut());
+ assertTrue(ColorSpace.get(ColorSpace.Named.DCI_P3).isWideGamut());
+ assertTrue(ColorSpace.get(ColorSpace.Named.BT2020).isWideGamut());
+ assertTrue(ColorSpace.get(ColorSpace.Named.ACES).isWideGamut());
+ assertTrue(ColorSpace.get(ColorSpace.Named.CIE_LAB).isWideGamut());
+ assertTrue(ColorSpace.get(ColorSpace.Named.CIE_XYZ).isWideGamut());
+ }
+
+ @Test
+ public void testWhitePoint() {
+ ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float[] whitePoint = cs.getWhitePoint();
+
+ assertNotNull(whitePoint);
+ assertEquals(2, whitePoint.length);
+
+ // Make sure a copy is returned
+ Arrays.fill(whitePoint, Float.NaN);
+ assertArrayNotEquals(whitePoint, cs.getWhitePoint(), 1e-5f);
+ assertSame(whitePoint, cs.getWhitePoint(whitePoint));
+ assertArrayEquals(whitePoint, cs.getWhitePoint(), 1e-5f);
+ }
+
+ @Test
+ public void testPrimaries() {
+ ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float[] primaries = cs.getPrimaries();
+
+ assertNotNull(primaries);
+ assertEquals(6, primaries.length);
+
+ // Make sure a copy is returned
+ Arrays.fill(primaries, Float.NaN);
+ assertArrayNotEquals(primaries, cs.getPrimaries(), 1e-5f);
+ assertSame(primaries, cs.getPrimaries(primaries));
+ assertArrayEquals(primaries, cs.getPrimaries(), 1e-5f);
+ }
+
+ @Test
+ public void testRGBtoXYZMatrix() {
+ ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float[] rgbToXYZ = cs.getTransform();
+
+ assertNotNull(rgbToXYZ);
+ assertEquals(9, rgbToXYZ.length);
+
+ // Make sure a copy is returned
+ Arrays.fill(rgbToXYZ, Float.NaN);
+ assertArrayNotEquals(rgbToXYZ, cs.getTransform(), 1e-5f);
+ assertSame(rgbToXYZ, cs.getTransform(rgbToXYZ));
+ assertArrayEquals(rgbToXYZ, cs.getTransform(), 1e-5f);
+ }
+
+ @Test
+ public void testXYZtoRGBMatrix() {
+ ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float[] xyzToRGB = cs.getInverseTransform();
+
+ assertNotNull(xyzToRGB);
+ assertEquals(9, xyzToRGB.length);
+
+ // Make sure a copy is returned
+ Arrays.fill(xyzToRGB, Float.NaN);
+ assertArrayNotEquals(xyzToRGB, cs.getInverseTransform(), 1e-5f);
+ assertSame(xyzToRGB, cs.getInverseTransform(xyzToRGB));
+ assertArrayEquals(xyzToRGB, cs.getInverseTransform(), 1e-5f);
+ }
+
+ @Test
+ public void testRGBtoXYZ() {
+ ColorSpace cs = ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float[] source = {0.75f, 0.5f, 0.25f};
+ float[] expected = {0.3012f, 0.2679f, 0.0840f};
+
+ float[] r1 = cs.toXyz(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayNotEquals(source, r1, 1e-5f);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ float[] r3 = {source[0], source[1], source[2]};
+ assertSame(r3, cs.toXyz(r3));
+ assertEquals(3, r3.length);
+ assertArrayEquals(r1, r3, 1e-5f);
+ }
+
+ @Test
+ public void testXYZtoRGB() {
+ ColorSpace cs = ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float[] source = {0.3012f, 0.2679f, 0.0840f};
+ float[] expected = {0.75f, 0.5f, 0.25f};
+
+ float[] r1 = cs.fromXyz(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayNotEquals(source, r1, 1e-5f);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ float[] r3 = {source[0], source[1], source[2]};
+ assertSame(r3, cs.fromXyz(r3));
+ assertEquals(3, r3.length);
+ assertArrayEquals(r1, r3, 1e-5f);
+ }
+
+ @Test
+ public void testConnect() {
+ ColorSpace.Connector connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.SRGB), ColorSpace.get(ColorSpace.Named.DCI_P3));
+
+ assertSame(ColorSpace.get(ColorSpace.Named.SRGB), connector.getSource());
+ assertSame(ColorSpace.get(ColorSpace.Named.DCI_P3), connector.getDestination());
+ assertSame(ColorSpace.RenderIntent.PERCEPTUAL, connector.getRenderIntent());
+
+ connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.SRGB), ColorSpace.get(ColorSpace.Named.SRGB));
+
+ assertSame(connector.getDestination(), connector.getSource());
+ assertSame(ColorSpace.RenderIntent.RELATIVE, connector.getRenderIntent());
+
+ connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.DCI_P3));
+ assertSame(ColorSpace.get(ColorSpace.Named.SRGB), connector.getDestination());
+
+ connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.SRGB));
+ assertSame(connector.getSource(), connector.getDestination());
+ }
+
+ @Test
+ public void testConnector() {
+ // Connect color spaces with same white points
+ ColorSpace.Connector connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.SRGB), ColorSpace.get(ColorSpace.Named.ADOBE_RGB));
+
+ float[] source = {1.0f, 0.5f, 0.0f};
+ float[] expected = {0.8912f, 0.4962f, 0.1164f};
+
+ float[] r1 = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayNotEquals(source, r1, 1e-5f);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ float[] r3 = {source[0], source[1], source[2]};
+ assertSame(r3, connector.transform(r3));
+ assertEquals(3, r3.length);
+ assertArrayEquals(r1, r3, 1e-5f);
+
+ connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.ADOBE_RGB), ColorSpace.get(ColorSpace.Named.SRGB));
+
+ float[] tmp = source;
+ source = expected;
+ expected = tmp;
+
+ r1 = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayNotEquals(source, r1, 1e-5f);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ r3 = new float[] {source[0], source[1], source[2]};
+ assertSame(r3, connector.transform(r3));
+ assertEquals(3, r3.length);
+ assertArrayEquals(r1, r3, 1e-5f);
+ }
+
+ @Test
+ public void testAdaptedConnector() {
+ // Connect color spaces with different white points
+ ColorSpace.Connector connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.SRGB), ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB));
+
+ float[] source = new float[] {1.0f, 0.0f, 0.0f};
+ float[] expected = new float[] {0.70226f, 0.2757f, 0.1036f};
+
+ float[] r = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r);
+ assertEquals(3, r.length);
+ assertArrayNotEquals(source, r, 1e-5f);
+ assertArrayEquals(expected, r, 1e-4f);
+ }
+
+ @Test
+ public void testAdaptedConnectorWithRenderIntent() {
+ // Connect a wider color space to a narrow color space
+ ColorSpace.Connector connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.DCI_P3),
+ ColorSpace.get(ColorSpace.Named.SRGB),
+ ColorSpace.RenderIntent.RELATIVE);
+
+ float[] source = {0.9f, 0.9f, 0.9f};
+
+ float[] relative = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(relative);
+ assertEquals(3, relative.length);
+ assertArrayNotEquals(source, relative, 1e-5f);
+ assertArrayEquals(new float[] {0.8862f, 0.8862f, 0.8862f}, relative, 1e-4f);
+
+ connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.DCI_P3),
+ ColorSpace.get(ColorSpace.Named.SRGB),
+ ColorSpace.RenderIntent.ABSOLUTE);
+
+ float[] absolute = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(absolute);
+ assertEquals(3, absolute.length);
+ assertArrayNotEquals(source, absolute, 1e-5f);
+ assertArrayNotEquals(relative, absolute, 1e-5f);
+ assertArrayEquals(new float[] {0.8475f, 0.9217f, 0.8203f}, absolute, 1e-4f);
+ }
+
+ @Test
+ public void testIdentityConnector() {
+ ColorSpace.Connector connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.SRGB), ColorSpace.get(ColorSpace.Named.SRGB));
+
+ assertSame(connector.getSource(), connector.getDestination());
+ assertSame(ColorSpace.RenderIntent.RELATIVE, connector.getRenderIntent());
+
+ float[] source = new float[] {0.11112f, 0.22227f, 0.444448f};
+
+ float[] r = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r);
+ assertEquals(3, r.length);
+ assertArrayEquals(source, r, 1e-5f);
+ }
+
+ @Test
+ public void testConnectorTransformIdentity() {
+ ColorSpace.Connector connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.DCI_P3), ColorSpace.get(ColorSpace.Named.DCI_P3));
+
+ float[] source = {1.0f, 0.0f, 0.0f};
+ float[] expected = {1.0f, 0.0f, 0.0f};
+
+ float[] r1 = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ float[] r3 = {source[0], source[1], source[2]};
+ assertSame(r3, connector.transform(r3));
+ assertEquals(3, r3.length);
+ assertArrayEquals(r1, r3, 1e-5f);
+ }
+
+ @Test
+ public void testAdaptation() {
+ ColorSpace adapted =
+ ColorSpace.adapt(ColorSpace.get(ColorSpace.Named.SRGB), ColorSpace.ILLUMINANT_D50);
+
+ float[] sRGBD50 = {
+ 0.43602175f, 0.22247513f, 0.01392813f,
+ 0.38510883f, 0.71690667f, 0.09710153f,
+ 0.14308129f, 0.06061824f, 0.71415880f
+ };
+
+ assertArrayEquals(sRGBD50, ((ColorSpace.Rgb) adapted).getTransform(), 1e-7f);
+
+ adapted =
+ ColorSpace.adapt(
+ ColorSpace.get(ColorSpace.Named.SRGB),
+ ColorSpace.ILLUMINANT_D50,
+ ColorSpace.Adaptation.BRADFORD);
+ assertArrayEquals(sRGBD50, ((ColorSpace.Rgb) adapted).getTransform(), 1e-7f);
+ }
+
+ @Test
+ public void testImplicitSRGBConnector() {
+ ColorSpace.Connector connector1 = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.DCI_P3));
+
+ assertSame(ColorSpace.get(ColorSpace.Named.SRGB), connector1.getDestination());
+
+ ColorSpace.Connector connector2 =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.DCI_P3), ColorSpace.get(ColorSpace.Named.SRGB));
+
+ float[] source = {0.6f, 0.9f, 0.7f};
+ assertArrayEquals(
+ connector1.transform(source[0], source[1], source[2]),
+ connector2.transform(source[0], source[1], source[2]),
+ 1e-7f);
+ }
+
+ @Test
+ public void testLab() {
+ ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.CIE_LAB));
+
+ float[] source = {100.0f, 0.0f, 0.0f};
+ float[] expected = {1.0f, 1.0f, 1.0f};
+
+ float[] r1 = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ source = new float[] {100.0f, 0.0f, 54.0f};
+ expected = new float[] {1.0f, 0.9925f, 0.5762f};
+
+ float[] r2 = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r2);
+ assertEquals(3, r2.length);
+ assertArrayEquals(expected, r2, 1e-3f);
+
+ connector =
+ ColorSpace.connect(
+ ColorSpace.get(ColorSpace.Named.CIE_LAB), ColorSpace.RenderIntent.ABSOLUTE);
+
+ source = new float[] {100.0f, 0.0f, 0.0f};
+ expected = new float[] {1.0f, 0.9910f, 0.8651f};
+
+ r1 = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ source = new float[] {100.0f, 0.0f, 54.0f};
+ expected = new float[] {1.0f, 0.9853f, 0.4652f};
+
+ r2 = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r2);
+ assertEquals(3, r2.length);
+ assertArrayEquals(expected, r2, 1e-3f);
+ }
+
+ @Test
+ public void testXYZ() {
+ ColorSpace xyz = ColorSpace.get(ColorSpace.Named.CIE_XYZ);
+
+ float[] source = {0.32f, 0.43f, 0.54f};
+
+ float[] r1 = xyz.toXyz(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayEquals(source, r1, 1e-7f);
+
+ float[] r2 = xyz.fromXyz(source[0], source[1], source[2]);
+ assertNotNull(r2);
+ assertEquals(3, r2.length);
+ assertArrayEquals(source, r2, 1e-7f);
+
+ ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.CIE_XYZ));
+
+ float[] expected = {0.2280f, 0.7541f, 0.8453f};
+
+ float[] r3 = connector.transform(source[0], source[1], source[2]);
+ assertNotNull(r3);
+ assertEquals(3, r3.length);
+ assertArrayEquals(expected, r3, 1e-3f);
+ }
+
+ @Test
+ public void testIDs() {
+ // These cannot change
+ assertEquals(0, ColorSpace.get(ColorSpace.Named.SRGB).getId());
+ assertEquals(-1, ColorSpace.MIN_ID);
+ assertEquals(63, ColorSpace.MAX_ID);
+ }
+
+ @Test
+ public void testFromLinear() {
+ ColorSpace.Rgb colorSpace = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float[] source = {0.0f, 0.5f, 1.0f};
+ float[] expected = {0.0f, 0.7354f, 1.0f};
+
+ float[] r1 = colorSpace.fromLinear(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ float[] r2 = {source[0], source[1], source[2]};
+ assertSame(r2, colorSpace.fromLinear(r2));
+ assertEquals(3, r2.length);
+ assertArrayEquals(r1, r2, 1e-5f);
+ }
+
+ @Test
+ public void testToLinear() {
+ ColorSpace.Rgb colorSpace = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+
+ float[] source = {0.0f, 0.5f, 1.0f};
+ float[] expected = new float[] {0.0f, 0.2140f, 1.0f};
+
+ float[] r1 = colorSpace.toLinear(source[0], source[1], source[2]);
+ assertNotNull(r1);
+ assertEquals(3, r1.length);
+ assertArrayEquals(expected, r1, 1e-3f);
+
+ float[] r2 = new float[] {source[0], source[1], source[2]};
+ assertSame(r2, colorSpace.toLinear(r2));
+ assertEquals(3, r2.length);
+ assertArrayEquals(r1, r2, 1e-5f);
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testTransferParameters() {
+ ColorSpace.Rgb colorSpace = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+ assertNotNull(colorSpace.getTransferParameters());
+
+ colorSpace = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB);
+ assertNotNull(colorSpace.getTransferParameters());
+
+ colorSpace =
+ new ColorSpace.Rgb(
+ "Almost sRGB", SRGB_TO_XYZ, x -> Math.pow(x, 1.0f / 2.2f), x -> Math.pow(x, 2.2f));
+ assertNull(colorSpace.getTransferParameters());
+ }
+
+ @Test
+ @Config(minSdk = O, maxSdk = P)
+ public void testTransferParameters_o_p() {
+ ColorSpace.Rgb colorSpace = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
+ assertNotNull(colorSpace.getTransferParameters());
+
+ colorSpace = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB);
+ assertNull(colorSpace.getTransferParameters());
+ }
+
+ @Test
+ public void testIdempotentTransferFunctions() {
+ Arrays.stream(ColorSpace.Named.values())
+ .map(ColorSpace::get)
+ .filter(cs -> cs.getModel() == ColorSpace.Model.RGB)
+ .map(cs -> (ColorSpace.Rgb) cs)
+ .forEach(
+ cs -> {
+ float[] source = {0.0f, 0.5f, 1.0f};
+ float[] r = cs.fromLinear(cs.toLinear(source[0], source[1], source[2]));
+ assertArrayEquals(source, r, 1e-3f);
+ });
+ }
+
+ // TODO: update test with latest
+ // cts/tests/tests/graphics/src/android/graphics/cts/ColorSpaceTest.java from U
+ @Test
+ @Config(maxSdk = TIRAMISU)
+ public void testMatch() {
+ for (ColorSpace.Named named : ColorSpace.Named.values()) {
+ ColorSpace cs = ColorSpace.get(named);
+ if (cs.getModel() == ColorSpace.Model.RGB) {
+ ColorSpace.Rgb rgb = (ColorSpace.Rgb) cs;
+ // match() cannot match extended sRGB
+ if (!rgb.equals(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB))
+ && !rgb.equals(ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB))) {
+
+ // match() uses CIE XYZ D50
+ rgb = (ColorSpace.Rgb) ColorSpace.adapt(rgb, ColorSpace.ILLUMINANT_D50);
+ assertSame(cs, ColorSpace.match(rgb.getTransform(), rgb.getTransferParameters()));
+ }
+ }
+ }
+
+ assertSame(
+ ColorSpace.get(ColorSpace.Named.SRGB),
+ ColorSpace.match(
+ SRGB_TO_XYZ_D50,
+ new ColorSpace.Rgb.TransferParameters(
+ 1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4)));
+ }
+
+ @Test
+ @Config(minSdk = Q) // This does not exist on API O-P
+ public void testCctToXyz() {
+ // Verify that range listed as meaningful by the API return float arrays as expected.
+ for (int i = 1667; i <= 25000; i++) {
+ float[] result = ColorSpace.cctToXyz(i);
+ assertNotNull(result);
+ assertEquals(3, result.length);
+ }
+ }
+
+ private static float[] identityMatrix =
+ new float[] {
+ 1.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f
+ };
+
+ @Test
+ @Config(minSdk = Q) // This does not exist on API O-P
+ public void testChromaticAdaptation() {
+ for (ColorSpace.Adaptation adaptation : ColorSpace.Adaptation.values()) {
+ float[][] whitePoints = {
+ ColorSpace.ILLUMINANT_A,
+ ColorSpace.ILLUMINANT_B,
+ ColorSpace.ILLUMINANT_C,
+ ColorSpace.ILLUMINANT_D50,
+ ColorSpace.ILLUMINANT_D55,
+ ColorSpace.ILLUMINANT_D60,
+ ColorSpace.ILLUMINANT_D65,
+ ColorSpace.ILLUMINANT_D75,
+ ColorSpace.ILLUMINANT_E,
+ };
+ for (float[] srcWhitePoint : whitePoints) {
+ for (float[] dstWhitePoint : whitePoints) {
+ float[] result = ColorSpace.chromaticAdaptation(adaptation, srcWhitePoint, dstWhitePoint);
+ assertNotNull(result);
+ assertEquals(9, result.length);
+ if (Arrays.equals(srcWhitePoint, dstWhitePoint)) {
+ assertArrayEquals(identityMatrix, result, 0f);
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private void assertArrayNotEquals(float[] a, float[] b, float eps) {
+ for (int i = 0; i < a.length; i++) {
+ if (Float.compare(a[i], b[i]) == 0 || Math.abs(a[i] - b[i]) < eps) {
+ fail("Expected " + a[i] + ", received " + b[i]);
+ }
+ }
+ }
+
+ private void assertArrayEquals(float[] a, float[] b, float eps) {
+ for (int i = 0; i < a.length; i++) {
+ if (Float.compare(a[i], b[i]) != 0 && Math.abs(a[i] - b[i]) > eps) {
+ fail("Expected " + a[i] + ", received " + b[i]);
+ }
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorTest.java
new file mode 100644
index 000000000..30b866810
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeColorTest.java
@@ -0,0 +1,80 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Color;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeColorTest {
+
+ @Test
+ public void testRgb() {
+ int color = Color.rgb(160, 160, 160);
+ assertThat(color).isEqualTo(-6250336);
+ }
+
+ @Test
+ public void testArgb() {
+ int color = Color.argb(100, 160, 160, 160);
+ assertThat(color).isEqualTo(1688248480);
+ }
+
+ @Test
+ public void testParseColor() {
+ assertThat(Color.parseColor("#ffffffff")).isEqualTo(-1);
+ assertThat(Color.parseColor("#00000000")).isEqualTo(0);
+
+ assertThat(Color.parseColor("#ffaabbcc")).isEqualTo(-5588020);
+ }
+
+ @Test
+ public void testParseColorWithStringName() {
+ assertThat(Color.parseColor("blue")).isEqualTo(-16776961);
+ assertThat(Color.parseColor("black")).isEqualTo(-16777216);
+ assertThat(Color.parseColor("green")).isEqualTo(-16711936);
+ }
+
+ @Test
+ public void testColorToHSVShouldBeCorrectForBlue() {
+ float[] hsv = new float[3];
+ Color.colorToHSV(Color.BLUE, hsv);
+
+ assertThat(hsv[0]).isEqualTo(240f);
+ assertThat(hsv[1]).isEqualTo(1.0f);
+ assertThat(hsv[2]).isEqualTo(1.0f);
+ }
+
+ @Test
+ public void testColorToHSVShouldBeCorrectForBlack() {
+ float[] hsv = new float[3];
+ Color.colorToHSV(Color.BLACK, hsv);
+
+ assertThat(hsv[0]).isEqualTo(0f);
+ assertThat(hsv[1]).isEqualTo(0f);
+ assertThat(hsv[2]).isEqualTo(0f);
+ }
+
+ @Test
+ public void testRGBToHSVShouldBeCorrectForBlue() {
+ float[] hsv = new float[3];
+ Color.RGBToHSV(0, 0, 255, hsv);
+
+ assertThat(hsv[0]).isEqualTo(240f);
+ assertThat(hsv[1]).isEqualTo(1.0f);
+ assertThat(hsv[2]).isEqualTo(1.0f);
+ }
+
+ @Test
+ public void testHSVToColorShouldReverseColorToHSV() {
+ float[] hsv = new float[3];
+ Color.colorToHSV(Color.RED, hsv);
+
+ assertThat(Color.HSVToColor(hsv)).isEqualTo(Color.RED);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeComposePathEffectTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeComposePathEffectTest.java
new file mode 100644
index 000000000..ed5a11a1b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeComposePathEffectTest.java
@@ -0,0 +1,70 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ComposePathEffect;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathEffect;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeComposePathEffectTest {
+ private static final int BITMAP_WIDTH = 110;
+ private static final int BITMAP_HEIGHT = 20;
+ private static final int START_X = 10;
+ private static final int END_X = BITMAP_WIDTH - 10;
+ private static final int CENTER = BITMAP_HEIGHT / 2;
+
+ @Test
+ public void testComposePathEffect() {
+ Path path = new Path();
+ path.moveTo(START_X, CENTER);
+ path.lineTo(END_X, CENTER);
+
+ // ----- ----- ----- -----
+ PathEffect innerEffect = new DashPathEffect(new float[] {25, 5}, 0);
+ // outer effect is applied to every segment of the inner effect
+ // --- --- --- ---
+ PathEffect outerEffect = new DashPathEffect(new float[] {15, 5}, 0);
+ // --- - --- - --- - --- -
+ PathEffect composedEffect = new ComposePathEffect(outerEffect, innerEffect);
+ // --- - --- - --- - --- -
+ PathEffect expectedEffect = new DashPathEffect(new float[] {15, 5, 5, 5}, 0);
+
+ Bitmap actual = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(actual);
+ Paint paint = makePaint();
+ paint.setPathEffect(composedEffect);
+ canvas.drawPath(path, paint);
+
+ Bitmap expected = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
+ canvas = new Canvas(expected);
+ paint = makePaint();
+ paint.setPathEffect(expectedEffect);
+ canvas.drawPath(path, paint);
+
+ for (int y = 0; y < BITMAP_HEIGHT; y++) {
+ for (int x = 0; x < BITMAP_WIDTH; x++) {
+ assertEquals(expected.getPixel(x, y), actual.getPixel(x, y));
+ }
+ }
+ }
+
+ private Paint makePaint() {
+ Paint paint = new Paint();
+ paint.setColor(Color.GREEN);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(0);
+ return paint;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeComposeShaderTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeComposeShaderTest.java
new file mode 100644
index 000000000..6a6df65a7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeComposeShaderTest.java
@@ -0,0 +1,176 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.BlendMode;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ComposeShader;
+import android.graphics.LinearGradient;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Shader;
+import android.graphics.Shader.TileMode;
+import android.util.Log;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeComposeShaderTest {
+ private static final int SIZE = 255;
+ private static final int TOLERANCE = 5;
+
+ @Test
+ public void testPorterDuff() {
+ LinearGradient blueGradient =
+ new LinearGradient(0, 0, SIZE, 0, Color.GREEN, Color.BLUE, Shader.TileMode.CLAMP);
+ LinearGradient redGradient =
+ new LinearGradient(0, 0, 0, SIZE, Color.GREEN, Color.RED, Shader.TileMode.CLAMP);
+ ComposeShader shader = new ComposeShader(blueGradient, redGradient, PorterDuff.Mode.SCREEN);
+
+ Bitmap bitmap = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint();
+ paint.setShader(shader);
+ canvas.drawPaint(paint);
+
+ for (int y = 0; y < SIZE; y++) {
+ for (int x = 0; x < SIZE; x++) {
+ float greenX = 1f - (x / 255f);
+ float greenY = 1f - (y / 255f);
+ int green = (int) ((greenX + greenY - greenX * greenY) * 255);
+ int pixel = bitmap.getPixel(x, y);
+ try {
+ assertEquals(0xFF, Color.alpha(pixel), TOLERANCE);
+ assertEquals(y, Color.red(pixel), TOLERANCE);
+ assertEquals(green, Color.green(pixel), TOLERANCE);
+ assertEquals(x, Color.blue(pixel), TOLERANCE);
+ } catch (Error e) {
+ Log.w(getClass().getName(), "Failed at (" + x + "," + y + ")");
+ throw e;
+ }
+ }
+ }
+ }
+
+ @Test
+ @Config(minSdk = Q) // This did not exist until Q
+ public void testBlendMode() {
+ LinearGradient blueGradient =
+ new LinearGradient(0, 0, SIZE, 0, Color.GREEN, Color.BLUE, Shader.TileMode.CLAMP);
+ LinearGradient redGradient =
+ new LinearGradient(0, 0, 0, SIZE, Color.GREEN, Color.RED, Shader.TileMode.CLAMP);
+ ComposeShader shader = new ComposeShader(blueGradient, redGradient, BlendMode.SCREEN);
+
+ Bitmap bitmap = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint();
+ paint.setShader(shader);
+ canvas.drawPaint(paint);
+
+ for (int y = 0; y < SIZE; y++) {
+ for (int x = 0; x < SIZE; x++) {
+ float greenX = 1f - (x / 255f);
+ float greenY = 1f - (y / 255f);
+ int green = (int) ((greenX + greenY - greenX * greenY) * 255);
+ int pixel = bitmap.getPixel(x, y);
+ try {
+ assertEquals(0xFF, Color.alpha(pixel), TOLERANCE);
+ assertEquals(y, Color.red(pixel), TOLERANCE);
+ assertEquals(green, Color.green(pixel), TOLERANCE);
+ assertEquals(x, Color.blue(pixel), TOLERANCE);
+ } catch (Error e) {
+ Log.w(getClass().getName(), "Failed at (" + x + "," + y + ")");
+ throw e;
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testXfermode() {
+ Bitmap redBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ redBitmap.eraseColor(Color.RED);
+ Bitmap cyanBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ cyanBitmap.eraseColor(Color.CYAN);
+
+ BitmapShader redShader = new BitmapShader(redBitmap, TileMode.CLAMP, TileMode.CLAMP);
+ BitmapShader cyanShader = new BitmapShader(cyanBitmap, TileMode.CLAMP, TileMode.CLAMP);
+
+ PorterDuffXfermode xferMode = new PorterDuffXfermode(PorterDuff.Mode.ADD);
+
+ ComposeShader shader = new ComposeShader(redShader, cyanShader, xferMode);
+
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint();
+ paint.setShader(shader);
+ canvas.drawPaint(paint);
+
+ // red + cyan = white
+ assertEquals(Color.WHITE, bitmap.getPixel(0, 0));
+ }
+
+ @Test
+ public void testChildLocalMatrix() {
+ Matrix translate1x1 = new Matrix();
+ translate1x1.setTranslate(1, 1);
+ Matrix translate0x1 = new Matrix();
+ translate0x1.setTranslate(0, 1);
+ Matrix translate1x0 = new Matrix();
+ translate1x0.setTranslate(1, 0);
+
+ Bitmap redBitmap = Bitmap.createBitmap(3, 3, Bitmap.Config.ARGB_8888);
+ redBitmap.setPixel(1, 1, Color.RED);
+ BitmapShader redShader = new BitmapShader(redBitmap, TileMode.CLAMP, TileMode.CLAMP);
+
+ Bitmap cyanBitmap = Bitmap.createBitmap(3, 3, Bitmap.Config.ARGB_8888);
+ cyanBitmap.setPixel(1, 1, Color.CYAN);
+ BitmapShader cyanShader = new BitmapShader(cyanBitmap, TileMode.CLAMP, TileMode.CLAMP);
+
+ ComposeShader composeShader = new ComposeShader(redShader, cyanShader, PorterDuff.Mode.ADD);
+
+ Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint();
+ paint.setShader(composeShader);
+
+ // initial state, white pixel from red and cyan overlap
+ bitmap.eraseColor(Color.TRANSPARENT);
+ canvas.drawPaint(paint);
+ assertEquals(Color.WHITE, bitmap.getPixel(1, 1));
+
+ // offset right+down from inner shaders
+ redShader.setLocalMatrix(translate1x1);
+ cyanShader.setLocalMatrix(translate1x1);
+ bitmap.eraseColor(Color.TRANSPARENT);
+ canvas.drawPaint(paint);
+ assertEquals(Color.WHITE, bitmap.getPixel(2, 2));
+
+ // offset right+down from outer shader
+ redShader.setLocalMatrix(null);
+ cyanShader.setLocalMatrix(null);
+ composeShader.setLocalMatrix(translate1x1);
+ bitmap.eraseColor(Color.TRANSPARENT);
+ canvas.drawPaint(paint);
+ assertEquals(Color.WHITE, bitmap.getPixel(2, 2));
+
+ // combine matrices from both levels
+ redShader.setLocalMatrix(translate0x1);
+ cyanShader.setLocalMatrix(null);
+ composeShader.setLocalMatrix(translate1x0);
+ bitmap.eraseColor(Color.TRANSPARENT);
+ canvas.drawPaint(paint);
+ assertEquals(Color.RED, bitmap.getPixel(2, 2));
+ assertEquals(Color.CYAN, bitmap.getPixel(2, 1));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeCornerPathEffectTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeCornerPathEffectTest.java
new file mode 100644
index 000000000..e0fdb68c8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeCornerPathEffectTest.java
@@ -0,0 +1,95 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.PathEffect;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeCornerPathEffectTest {
+ private static final int BITMAP_WIDTH = 100;
+ private static final int BITMAP_HEIGHT = 100;
+ private static final int PADDING = 10;
+ private static final int RADIUS = 20;
+ private static final int TOLERANCE = 5;
+
+ @Test
+ public void testCornerPathEffect() {
+ Path path = new Path();
+ path.moveTo(0, PADDING);
+ path.lineTo(BITMAP_WIDTH - PADDING, PADDING);
+ path.lineTo(BITMAP_WIDTH - PADDING, BITMAP_HEIGHT);
+
+ PathEffect effect = new CornerPathEffect(RADIUS);
+
+ Paint pathPaint = new Paint();
+ pathPaint.setColor(Color.GREEN);
+ pathPaint.setStyle(Style.STROKE);
+ pathPaint.setStrokeWidth(0);
+ pathPaint.setPathEffect(effect);
+ pathPaint.setAntiAlias(false);
+
+ Bitmap bitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ // draw the path using the corner path effect
+ canvas.drawPath(path, pathPaint);
+
+ // create a path that describes the expected shape after the corner is rounded
+ Path expectedPath = new Path();
+ RectF oval =
+ new RectF(
+ BITMAP_WIDTH - PADDING - 2 * RADIUS,
+ PADDING,
+ BITMAP_WIDTH - PADDING,
+ PADDING + 2 * RADIUS);
+ expectedPath.moveTo(0, PADDING);
+ expectedPath.arcTo(oval, 270, 90);
+ expectedPath.lineTo(BITMAP_WIDTH - PADDING, BITMAP_HEIGHT);
+
+ // A paint that draws the expected path with a tolerance width into the red channel
+ Paint expectedPaint = new Paint();
+ expectedPaint.setColor(Color.RED);
+ expectedPaint.setStyle(Style.STROKE);
+ expectedPaint.setStrokeWidth(TOLERANCE);
+ expectedPaint.setXfermode(new PorterDuffXfermode(Mode.SCREEN));
+
+ canvas.drawPath(expectedPath, expectedPaint);
+
+ int numPixels = 0;
+ for (int y = 0; y < BITMAP_HEIGHT; y++) {
+ for (int x = 0; x < BITMAP_WIDTH; x++) {
+ int pixel = bitmap.getPixel(x, y);
+ if (Color.green(pixel) > 0) {
+ numPixels += 1;
+ // green path must overlap with red guide line
+ assertEquals(Color.YELLOW, pixel);
+ }
+ }
+ }
+ // number of pixels that should be on a straight line
+ int straightLines = BITMAP_WIDTH - PADDING - RADIUS + BITMAP_HEIGHT - PADDING - RADIUS;
+ // number of pixels forming the corner
+ int cornerPixels = numPixels - straightLines;
+ // rounded corner must have less pixels than a sharp corner
+ assertTrue(cornerPixels < 2 * RADIUS);
+ // ... but not as few as a diagonal
+ assertTrue(cornerPixels > Math.sqrt(2 * Math.pow(RADIUS, 2)));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeDashPathEffectTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeDashPathEffectTest.java
new file mode 100644
index 000000000..c1f6491f7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeDashPathEffectTest.java
@@ -0,0 +1,102 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.PathEffect;
+import android.util.Log;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeDashPathEffectTest {
+ private static final int BITMAP_WIDTH = 200;
+ private static final int BITMAP_HEIGHT = 20;
+ private static final int START_X = 10;
+ private static final int END_X = BITMAP_WIDTH - START_X;
+ private static final int COORD_Y = BITMAP_HEIGHT / 2;
+ private static final float[] PATTERN = new float[] {15, 5, 10, 5};
+ private static final int OFFSET = 5;
+ private static final int BACKGROUND = Color.TRANSPARENT;
+ private static final int FOREGROUND = Color.GREEN;
+
+ @Test
+ public void testDashPathEffect() {
+ PathEffect effect = new DashPathEffect(PATTERN, OFFSET);
+ Bitmap bitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(BACKGROUND);
+
+ Path path = new Path();
+ path.moveTo(START_X, COORD_Y);
+ path.lineTo(END_X, COORD_Y);
+
+ Paint paint = new Paint();
+ paint.setStyle(Style.STROKE);
+ paint.setStrokeWidth(0);
+ paint.setColor(FOREGROUND);
+ paint.setPathEffect(effect);
+ paint.setAntiAlias(false);
+
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawPath(path, paint);
+
+ PatternIterator iterator = new PatternIterator(PATTERN, OFFSET);
+ for (int y = 0; y < BITMAP_HEIGHT; y++) {
+ for (int x = 0; x < BITMAP_WIDTH; x++) {
+ try {
+ if (y == COORD_Y && x >= START_X && x < END_X) {
+ if (iterator.next()) {
+ assertEquals(FOREGROUND, bitmap.getPixel(x, y));
+ } else {
+ assertEquals(BACKGROUND, bitmap.getPixel(x, y));
+ }
+ } else {
+ assertEquals(BACKGROUND, bitmap.getPixel(x, y));
+ }
+ } catch (Error e) {
+ Log.w(getClass().getName(), "Failed at (" + x + "," + y + ")");
+ throw e;
+ }
+ }
+ }
+ }
+
+ private static class PatternIterator {
+ private int mPatternOffset;
+ private int mLength;
+ private final float[] mPattern;
+
+ /** Create an instance that iterates through the given pattern starting at the given offset. */
+ PatternIterator(final float[] pattern, int offset) {
+ mPattern = pattern;
+ while (offset-- > 0) {
+ next();
+ }
+ }
+
+ /** Determine whether to draw the current pixel and move on to the next. */
+ @CanIgnoreReturnValue
+ boolean next() {
+ int oldPatternOffset = mPatternOffset;
+ mLength += 1;
+ if (mLength == mPattern[mPatternOffset]) {
+ mLength = 0;
+ mPatternOffset += 1;
+ mPatternOffset %= mPattern.length;
+ }
+ // even offsets are 'on'
+ return (oldPatternOffset & 1) == 0;
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeDiscretePathEffectTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeDiscretePathEffectTest.java
new file mode 100644
index 000000000..f471aabf5
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeDiscretePathEffectTest.java
@@ -0,0 +1,96 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DiscretePathEffect;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeDiscretePathEffectTest {
+ private static final int BITMAP_WIDTH = 200;
+ private static final int BITMAP_HEIGHT = 100;
+ private static final int START_X = 10;
+ private static final int END_X = BITMAP_WIDTH - START_X;
+ private static final int COORD_Y = BITMAP_HEIGHT / 2;
+ private static final int SEGMENT_LENGTH = 10; // must be < BITMAP_WIDTH
+ private static final int DEVIATION = 10; // must be < BITMAP_HEIGHT
+
+ @Test
+ public void testDiscretePathEffect() {
+ DiscretePathEffect effect = new DiscretePathEffect(SEGMENT_LENGTH, DEVIATION);
+
+ Paint paint = new Paint();
+ paint.setColor(Color.GREEN);
+ paint.setStyle(Style.STROKE);
+ paint.setStrokeWidth(0);
+ paint.setPathEffect(effect);
+ paint.setAntiAlias(false);
+
+ Path path = new Path();
+ path.moveTo(START_X, COORD_Y);
+ path.lineTo(END_X, COORD_Y);
+
+ Bitmap bitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.TRANSPARENT);
+
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawPath(path, paint);
+
+ // draw guide line into red channel (each segment should cross this once)
+ paint = new Paint();
+ paint.setColor(Color.RED);
+ paint.setStyle(Style.STROKE);
+ paint.setStrokeWidth(0);
+ paint.setXfermode(new PorterDuffXfermode(Mode.SCREEN));
+ canvas.drawPath(path, paint);
+
+ // draw guide rectangle into blue channel (each segment must be completely inside this)
+ paint.setColor(Color.BLUE);
+ paint.setStrokeWidth(1 + 2 * DEVIATION);
+ canvas.drawPath(path, paint);
+
+ int intersect = 0;
+ int numGreenPixels = 0;
+ int minY = BITMAP_HEIGHT;
+ int maxY = 0;
+ for (int y = 0; y < BITMAP_HEIGHT; y++) {
+ for (int x = 0; x < BITMAP_WIDTH; x++) {
+ int pixel = bitmap.getPixel(x, y);
+ if (Color.green(pixel) > 0) {
+ numGreenPixels += 1;
+ minY = Math.min(minY, y);
+ maxY = Math.max(maxY, y);
+ assertEquals(0xFF, Color.blue(pixel));
+ if (Color.red(pixel) > 0) {
+ intersect += 1;
+ }
+ }
+ }
+ }
+ int lineLength = END_X - START_X;
+ // the number of pixels in all segments must be at least the same as the line length
+ assertTrue(numGreenPixels >= lineLength);
+ // green line must vary in y direction
+ assertTrue(maxY - minY > 0);
+ // ... but not too much
+ assertTrue(maxY - minY <= 1 + 2 * DEVIATION);
+ // intersecting pixels must be less than line length, otherwise deviation doesn't work
+ assertTrue(intersect < lineLength);
+ // there must be at least as many intersecting pixels as there are full segments
+ assertTrue(intersect >= lineLength / SEGMENT_LENGTH);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeEmbossMaskFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeEmbossMaskFilterTest.java
new file mode 100644
index 000000000..00e8a7127
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeEmbossMaskFilterTest.java
@@ -0,0 +1,83 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.EmbossMaskFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeEmbossMaskFilterTest {
+ private static final int BITMAP_WIDTH = 100;
+ private static final int BITMAP_HEIGHT = 100;
+ private static final int START_X = 10;
+ private static final int END_X = BITMAP_WIDTH - START_X;
+ private static final int CENTER_X = (START_X + END_X) / 2;
+ private static final int CENTER_Y = BITMAP_HEIGHT / 2;
+ private static final int STROKE_WIDTH = 10;
+
+ @Test
+ public void testEmbossMaskFilter() {
+ EmbossMaskFilter filter = new EmbossMaskFilter(new float[] {1, 1, 1}, 0.5f, 8, 3);
+
+ Paint paint = new Paint();
+ paint.setMaskFilter(filter);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(STROKE_WIDTH);
+ paint.setColor(Color.GRAY);
+
+ Path path = new Path();
+ path.moveTo(START_X, CENTER_Y);
+ path.lineTo(END_X, CENTER_Y);
+
+ Bitmap bitmap = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.BLACK);
+
+ Canvas c = new Canvas(bitmap);
+ c.drawPath(path, paint);
+
+ Rect top = new Rect(0, 0, BITMAP_WIDTH, CENTER_Y);
+ Rect bottom = new Rect(0, CENTER_Y, BITMAP_WIDTH, BITMAP_HEIGHT);
+ Rect left = new Rect(0, 0, CENTER_X, BITMAP_HEIGHT);
+ Rect right = new Rect(CENTER_X, 0, BITMAP_WIDTH, BITMAP_HEIGHT);
+
+ assertTrue(brightness(bitmap, top) > brightness(bitmap, bottom));
+ assertTrue(brightness(bitmap, left) > brightness(bitmap, right));
+
+ // emboss must not change anything outside the drawn shape
+ top.bottom = CENTER_Y - STROKE_WIDTH / 2;
+ assertEquals(0, brightness(bitmap, top));
+ bottom.top = CENTER_Y + STROKE_WIDTH / 2;
+ assertEquals(0, brightness(bitmap, bottom));
+ left.right = START_X;
+ assertEquals(0, brightness(bitmap, left));
+ right.left = END_X;
+ assertEquals(0, brightness(bitmap, right));
+ }
+
+ /**
+ * Calculate the cumulative brightness of all pixels in the given rectangle. Ignores alpha
+ * channel. Maximum returned value depends on the size of the rectangle.
+ */
+ private long brightness(Bitmap b, Rect rect) {
+ long color = 0;
+ for (int y = rect.top; y < rect.bottom; y++) {
+ for (int x = rect.left; x < rect.right; x++) {
+ int pixel = b.getPixel(x, y);
+ color += Color.red(pixel) + Color.green(pixel) + Color.blue(pixel);
+ }
+ }
+ return color;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeFontFamilyTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeFontFamilyTest.java
new file mode 100644
index 000000000..31e1d69ac
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeFontFamilyTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.content.res.AssetManager;
+import android.graphics.fonts.Font;
+import android.graphics.fonts.FontFamily;
+import android.graphics.fonts.FontStyle;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = Q)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeFontFamilyTest {
+ private static final String FONT_DIR = "fonts/family_selection/ttf/";
+
+ @Test
+ public void testBuilder_singleFont() throws IOException {
+ AssetManager am = RuntimeEnvironment.getApplication().getAssets();
+ Font font = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ FontFamily family = new FontFamily.Builder(font).build();
+ assertNotNull(family);
+ assertEquals(1, family.getSize());
+ assertEquals(font, family.getFont(0));
+ }
+
+ @Test
+ @Config(sdk = 29)
+ public void testBuilder_multipleFont() throws IOException {
+ AssetManager am = RuntimeEnvironment.getApplication().getAssets();
+ Font regularFont = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ Font boldFont = new Font.Builder(am, FONT_DIR + "ascii_m3em_weight700_upright.ttf").build();
+ FontFamily family = new FontFamily.Builder(regularFont).addFont(boldFont).build();
+ assertNotNull(family);
+ assertEquals(2, family.getSize());
+ assertNotSame(family.getFont(0), family.getFont(1));
+ assertTrue(family.getFont(0).equals(regularFont) || family.getFont(0).equals(boldFont));
+ assertTrue(family.getFont(1).equals(regularFont) || family.getFont(1).equals(boldFont));
+ }
+
+ @Test
+ public void testBuilder_multipleFont_overrideWeight() throws IOException {
+ AssetManager am = RuntimeEnvironment.getApplication().getAssets();
+ Font regularFont = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ Font boldFont =
+ new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").setWeight(700).build();
+ FontFamily family = new FontFamily.Builder(regularFont).addFont(boldFont).build();
+ assertNotNull(family);
+ assertEquals(2, family.getSize());
+ assertNotSame(family.getFont(0), family.getFont(1));
+ assertTrue(family.getFont(0).equals(regularFont) || family.getFont(0).equals(boldFont));
+ assertTrue(family.getFont(1).equals(regularFont) || family.getFont(1).equals(boldFont));
+ }
+
+ @Test
+ public void testBuilder_multipleFont_overrideItalic() throws IOException {
+ AssetManager am = RuntimeEnvironment.getApplication().getAssets();
+ Font regularFont = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ Font italicFont =
+ new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf")
+ .setSlant(FontStyle.FONT_SLANT_ITALIC)
+ .build();
+ FontFamily family = new FontFamily.Builder(regularFont).addFont(italicFont).build();
+ assertNotNull(family);
+ assertEquals(2, family.getSize());
+ assertNotSame(family.getFont(0), family.getFont(1));
+ assertTrue(family.getFont(0).equals(regularFont) || family.getFont(0).equals(italicFont));
+ assertTrue(family.getFont(1).equals(regularFont) || family.getFont(1).equals(italicFont));
+ }
+
+ @Test
+ public void testBuilder_multipleFont_sameStyle() throws IOException {
+ AssetManager am = RuntimeEnvironment.getApplication().getAssets();
+ Font regularFont = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ Font regularFont2 = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new FontFamily.Builder(regularFont).addFont(regularFont2).build());
+ }
+
+ @Test
+ public void testBuilder_multipleFont_sameStyle_overrideWeight() throws IOException {
+ AssetManager am = RuntimeEnvironment.getApplication().getAssets();
+ Font regularFont = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ Font regularFont2 =
+ new Font.Builder(am, FONT_DIR + "ascii_m3em_weight700_upright.ttf").setWeight(400).build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new FontFamily.Builder(regularFont).addFont(regularFont2).build());
+ }
+
+ @Test
+ public void testBuilder_multipleFont_sameStyle_overrideItalic() throws IOException {
+ AssetManager am = RuntimeEnvironment.getApplication().getAssets();
+ Font regularFont = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ Font regularFont2 =
+ new Font.Builder(am, FONT_DIR + "ascii_h3em_weight400_italic.ttf")
+ .setSlant(FontStyle.FONT_SLANT_UPRIGHT)
+ .build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new FontFamily.Builder(regularFont).addFont(regularFont2).build());
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeFontTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeFontTest.java
new file mode 100644
index 000000000..fcd9ad2d6
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeFontTest.java
@@ -0,0 +1,35 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.fonts.Font;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = Q)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeFontTest {
+ private static final String FONT_DIR = "fonts/family_selection/ttf/";
+
+ @Test
+ public void initializeBuilderWithPath() throws Exception {
+ AssetManager am = RuntimeEnvironment.getApplication().getAssets();
+ Font font = new Font.Builder(am, FONT_DIR + "ascii_g3em_weight400_upright.ttf").build();
+ assertThat(font).isNotNull();
+ assertThat(font.getStyle().getWeight()).isEqualTo(400);
+ }
+
+ @Test
+ public void initializeBuilderWithResource() throws Exception {
+ Resources res = RuntimeEnvironment.getApplication().getResources();
+ Font font = new Font.Builder(res, R.font.a3em).build();
+ assertThat(font).isNotNull();
+ assertThat(font.getStyle().getWeight()).isEqualTo(400);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeHardwareRendererObserverTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeHardwareRendererObserverTest.java
new file mode 100644
index 000000000..7b348b213
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeHardwareRendererObserverTest.java
@@ -0,0 +1,36 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.S;
+
+import android.graphics.HardwareRendererObserver;
+import android.graphics.HardwareRendererObserver.OnFrameMetricsAvailableListener;
+import android.os.Handler;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+@Config(sdk = R)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeHardwareRendererObserverTest {
+
+ @Test
+ public void test_hardwareRenderer() {
+ OnFrameMetricsAvailableListener listener = i -> {};
+ if (RuntimeEnvironment.getApiLevel() >= S) {
+ HardwareRendererObserver unused =
+ new HardwareRendererObserver(listener, new long[0], new Handler(), false);
+ } else {
+ HardwareRendererObserver unused =
+ ReflectionHelpers.callConstructor(
+ HardwareRendererObserver.class,
+ ClassParameter.from(OnFrameMetricsAvailableListener.class, listener),
+ ClassParameter.from(long[].class, new long[0]),
+ ClassParameter.from(Handler.class, new Handler()));
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeHardwareRendererTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeHardwareRendererTest.java
new file mode 100644
index 000000000..2b6c48d7b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeHardwareRendererTest.java
@@ -0,0 +1,27 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.Q;
+
+import android.graphics.HardwareRenderer;
+import android.view.Choreographer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = Q)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeHardwareRendererTest {
+
+ @Test
+ public void test_hardwareRenderer() {
+ HardwareRenderer unused = new HardwareRenderer();
+ }
+
+ @Test
+ public void choreographer_firstCalled() {
+ // In some SDK levels, the Choreographer constructor ends up calling
+ // HardwareRenderer.nHackySetRTAnimationsEnabled. Ensure that RNG is loaded if this happens.
+ var unused = Choreographer.getInstance();
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeImageDecoderTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeImageDecoderTest.java
new file mode 100644
index 000000000..186aa305e
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeImageDecoderTest.java
@@ -0,0 +1,301 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.Q;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertSame;
+import static org.junit.Assert.assertEquals;
+
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.ImageDecoder;
+import android.graphics.ImageDecoder.Source;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.function.IntFunction;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = P)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeImageDecoderTest {
+ static final class Record {
+ public final int resId;
+ public final int width;
+ public final int height;
+ public final boolean isGray;
+ public final boolean hasAlpha;
+ public final String mimeType;
+ public final ColorSpace colorSpace;
+
+ Record(
+ int resId,
+ int width,
+ int height,
+ String mimeType,
+ boolean isGray,
+ boolean hasAlpha,
+ ColorSpace colorSpace) {
+ this.resId = resId;
+ this.width = width;
+ this.height = height;
+ this.mimeType = mimeType;
+ this.isGray = isGray;
+ this.hasAlpha = hasAlpha;
+ this.colorSpace = colorSpace;
+ }
+ }
+
+ private static final ColorSpace SRGB = ColorSpace.get(ColorSpace.Named.SRGB);
+
+ static Record[] getRecords() {
+ ArrayList<Record> records =
+ new ArrayList<>(
+ Arrays.asList(
+ new Record(R.drawable.baseline_jpeg, 1280, 960, "image/jpeg", false, false, SRGB),
+ new Record(R.drawable.grayscale_jpg, 128, 128, "image/jpeg", true, false, SRGB),
+ new Record(R.drawable.png_test, 640, 480, "image/png", false, false, SRGB),
+ new Record(R.drawable.gif_test, 320, 240, "image/gif", false, false, SRGB),
+ new Record(R.drawable.bmp_test, 320, 240, "image/bmp", false, false, SRGB),
+ new Record(R.drawable.webp_test, 640, 480, "image/webp", false, false, SRGB),
+ new Record(R.raw.sample_1mp, 600, 338, "image/x-adobe-dng", false, false, SRGB)));
+ return records.toArray(new Record[] {});
+ }
+
+ // offset is how many bytes to offset the beginning of the image.
+ // extra is how many bytes to append at the end.
+ private static byte[] getAsByteArray(int resId, int offset, int extra) {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ writeToStream(output, resId, offset, extra);
+ return output.toByteArray();
+ }
+
+ static byte[] getAsByteArray(int resId) {
+ return getAsByteArray(resId, 0, 0);
+ }
+
+ static void writeToStream(OutputStream output, int resId, int offset, int extra) {
+ InputStream input = getResources().openRawResource(resId);
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ try {
+ for (int i = 0; i < offset; ++i) {
+ output.write(0);
+ }
+
+ while ((bytesRead = input.read(buffer)) != -1) {
+ output.write(buffer, 0, bytesRead);
+ }
+
+ for (int i = 0; i < extra; ++i) {
+ output.write(0);
+ }
+
+ input.close();
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private ByteBuffer getAsByteBufferWrap(int resId) {
+ byte[] buffer = getAsByteArray(resId);
+ return ByteBuffer.wrap(buffer);
+ }
+
+ private ByteBuffer getAsDirectByteBuffer(int resId) {
+ byte[] buffer = getAsByteArray(resId);
+ ByteBuffer byteBuffer = ByteBuffer.allocateDirect(buffer.length);
+ byteBuffer.put(buffer);
+ byteBuffer.position(0);
+ return byteBuffer;
+ }
+
+ private ByteBuffer getAsReadOnlyByteBuffer(int resId) {
+ return getAsByteBufferWrap(resId).asReadOnlyBuffer();
+ }
+
+ private interface SourceCreator extends IntFunction<Source> {}
+
+ private SourceCreator[] creators =
+ new SourceCreator[] {
+ resId -> ImageDecoder.createSource(getAsByteArray(resId)),
+ resId -> ImageDecoder.createSource(getAsByteBufferWrap(resId)),
+ resId -> ImageDecoder.createSource(getAsDirectByteBuffer(resId)),
+ resId -> ImageDecoder.createSource(getAsReadOnlyByteBuffer(resId)),
+ };
+
+ private static Resources getResources() {
+ return RuntimeEnvironment.getApplication().getResources();
+ }
+
+ @Test
+ public void testInfo() throws Exception {
+ for (Record record : getRecords()) {
+ for (SourceCreator f : creators) {
+ ImageDecoder.Source src = f.apply(record.resId);
+ assertNotNull(src);
+ ImageDecoder.decodeDrawable(
+ src,
+ (decoder, info, s) -> {
+ assertEquals(record.width, info.getSize().getWidth());
+ assertEquals(record.height, info.getSize().getHeight());
+ assertEquals(record.mimeType, info.getMimeType());
+ assertSame(record.colorSpace, info.getColorSpace());
+ });
+ }
+ }
+ }
+
+ @Test
+ public void loadNinePatch() {
+ getResources().getDrawable(R.drawable.ninepatchdrawable);
+ }
+
+ static class AssetRecord {
+ public final String name;
+ public final int width;
+ public final int height;
+ public final boolean isF16;
+ public final boolean isGray;
+ public final boolean hasAlpha;
+ private final ColorSpace colorSpace;
+
+ AssetRecord(
+ String name,
+ int width,
+ int height,
+ boolean isF16,
+ boolean isGray,
+ boolean hasAlpha,
+ ColorSpace colorSpace) {
+ this.name = name;
+ this.width = width;
+ this.height = height;
+ this.isF16 = isF16;
+ this.isGray = isGray;
+ this.hasAlpha = hasAlpha;
+ this.colorSpace = colorSpace;
+ }
+
+ public ColorSpace getColorSpace() {
+ return colorSpace;
+ }
+
+ public void checkColorSpace(ColorSpace requested, ColorSpace actual) {
+ assertNotNull("Null ColorSpace for " + this.name, actual);
+ if (this.isF16 && requested != null) {
+ if (requested.equals(ColorSpace.get(ColorSpace.Named.LINEAR_SRGB))) {
+ assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB), actual);
+ } else if (requested.equals(ColorSpace.get(ColorSpace.Named.SRGB))) {
+ assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), actual);
+ } else {
+ assertSame(requested, actual);
+ }
+ } else if (requested != null) {
+ // If the asset is *not* 16 bit, requesting EXTENDED will promote to 16 bit.
+ assertSame(requested, actual);
+ } else if (colorSpace == null) {
+ assertEquals(this.name, "Unknown", actual.getName());
+ } else {
+ assertSame(this.name, colorSpace, actual);
+ }
+ }
+ }
+
+ static AssetRecord[] getAssetRecords() {
+ return new AssetRecord[] {
+ // A null ColorSpace means that the color space is "Unknown".
+ new AssetRecord("almost-red-adobe.png", 1, 1, false, false, false, null),
+ new AssetRecord(
+ "green-p3.png", 64, 64, false, false, false, ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
+ new AssetRecord("green-srgb.png", 64, 64, false, false, false, SRGB),
+ new AssetRecord(
+ "blue-16bit-prophoto.png",
+ 100,
+ 100,
+ true,
+ false,
+ true,
+ ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB)),
+ new AssetRecord(
+ "blue-16bit-srgb.png",
+ 64,
+ 64,
+ true,
+ false,
+ false,
+ ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)),
+ new AssetRecord("purple-cmyk.png", 64, 64, false, false, false, SRGB),
+ new AssetRecord("purple-displayprofile.png", 64, 64, false, false, false, null),
+ new AssetRecord(
+ "red-adobergb.png",
+ 64,
+ 64,
+ false,
+ false,
+ false,
+ ColorSpace.get(ColorSpace.Named.ADOBE_RGB)),
+ new AssetRecord(
+ "translucent-green-p3.png",
+ 64,
+ 64,
+ false,
+ false,
+ true,
+ ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
+ new AssetRecord(
+ "grayscale-linearSrgb.png",
+ 32,
+ 32,
+ false,
+ true,
+ false,
+ ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)),
+ new AssetRecord(
+ "grayscale-16bit-linearSrgb.png",
+ 32,
+ 32,
+ true,
+ false,
+ true,
+ ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB)),
+ };
+ }
+
+ @Test
+ public void testAssetSource() throws Exception {
+ for (AssetRecord record : getAssetRecords()) {
+ AssetManager assets = getResources().getAssets();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+ Bitmap bm =
+ ImageDecoder.decodeBitmap(
+ src,
+ (decoder, info, s) -> {
+ if (record.isF16) {
+ // CTS infrastructure fails to create F16 HARDWARE Bitmaps, so this
+ // switches to using software.
+ decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+ }
+
+ record.checkColorSpace(null, info.getColorSpace());
+ });
+ assertEquals(record.name, record.width, bm.getWidth());
+ assertEquals(record.name, record.height, bm.getHeight());
+ if (record.name.startsWith("blue-16bit") && RuntimeEnvironment.getApiLevel() >= Q) {
+ // This assertion fails for the "blue-16bit" images in Android P.
+ record.checkColorSpace(null, bm.getColorSpace());
+ }
+ assertEquals(record.hasAlpha, bm.hasAlpha());
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeInterpolatorTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeInterpolatorTest.java
new file mode 100644
index 000000000..9e0e91712
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeInterpolatorTest.java
@@ -0,0 +1,230 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.graphics.Interpolator;
+import android.graphics.Interpolator.Result;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowNativeInterpolatorTest {
+ private static final int DEFAULT_KEYFRAME_COUNT = 2;
+ private static final float TOLERANCE = 0.1f;
+
+ @Test
+ public void testConstructor() {
+ Interpolator interpolator = new Interpolator(10);
+ assertEquals(10, interpolator.getValueCount());
+ assertEquals(DEFAULT_KEYFRAME_COUNT, interpolator.getKeyFrameCount());
+
+ interpolator = new Interpolator(15, 20);
+ assertEquals(15, interpolator.getValueCount());
+ assertEquals(20, interpolator.getKeyFrameCount());
+ }
+
+ @Test
+ public void testReset1() {
+ final int expected = 100;
+ Interpolator interpolator = new Interpolator(10);
+ assertEquals(DEFAULT_KEYFRAME_COUNT, interpolator.getKeyFrameCount());
+ interpolator.reset(expected);
+ assertEquals(expected, interpolator.getValueCount());
+ assertEquals(DEFAULT_KEYFRAME_COUNT, interpolator.getKeyFrameCount());
+ }
+
+ @Test
+ public void testReset2() {
+ int expected1 = 100;
+ int expected2 = 200;
+ // new the Interpolator instance
+ Interpolator interpolator = new Interpolator(10);
+ interpolator.reset(expected1, expected2);
+ assertEquals(expected1, interpolator.getValueCount());
+ assertEquals(expected2, interpolator.getKeyFrameCount());
+ }
+
+ @Test
+ public void testTimeToValues2() {
+ Interpolator interpolator = new Interpolator(1);
+ interpolator.setKeyFrame(0, 2000, new float[] {1.0f});
+ interpolator.setKeyFrame(1, 4000, new float[] {2.0f});
+ verifyValue(1000, 1.0f, Result.FREEZE_START, interpolator);
+ verifyValue(3000, 1.5f, Result.NORMAL, interpolator);
+ verifyValue(6000, 2.0f, Result.FREEZE_END, interpolator);
+
+ // known bug: time argument is unsigned 32bit in graphics library
+ verifyValue(-1000, 2.0f, Result.FREEZE_END, interpolator);
+
+ interpolator.reset(1, 3);
+ interpolator.setKeyFrame(0, 2000, new float[] {1.0f});
+ interpolator.setKeyFrame(1, 4000, new float[] {2.0f});
+ interpolator.setKeyFrame(2, 6000, new float[] {4.0f});
+ verifyValue(0, 1.0f, Result.FREEZE_START, interpolator);
+ verifyValue(3000, 1.5f, Result.NORMAL, interpolator);
+ verifyValue(5000, 3.0f, Result.NORMAL, interpolator);
+ verifyValue(8000, 4.0f, Result.FREEZE_END, interpolator);
+
+ final int valueCount = 2;
+ final int validTime = 0;
+ interpolator.reset(valueCount);
+ assertEquals(valueCount, interpolator.getValueCount());
+ try {
+ // value array too short
+ interpolator.timeToValues(validTime, new float[valueCount - 1]);
+ fail("should throw out ArrayStoreException");
+ } catch (ArrayStoreException e) {
+ // expected
+ }
+
+ interpolator.reset(2, 2);
+ interpolator.setKeyFrame(0, 4000, new float[] {1.0f, 1.0f});
+ interpolator.setKeyFrame(1, 6000, new float[] {2.0f, 4.0f});
+ verifyValues(2000, new float[] {1.0f, 1.0f}, Result.FREEZE_START, interpolator);
+ verifyValues(5000, new float[] {1.5f, 2.5f}, Result.NORMAL, interpolator);
+ verifyValues(8000, new float[] {2.0f, 4.0f}, Result.FREEZE_END, interpolator);
+ }
+
+ @Test
+ public void testSetRepeatMirror() {
+ Interpolator interpolator = new Interpolator(1, 3);
+ interpolator.setKeyFrame(0, 2000, new float[] {1.0f});
+ interpolator.setKeyFrame(1, 4000, new float[] {2.0f});
+ interpolator.setKeyFrame(2, 6000, new float[] {4.0f});
+
+ verifyValue(1000, 1.0f, Result.FREEZE_START, interpolator);
+ verifyValue(3000, 1.5f, Result.NORMAL, interpolator);
+ verifyValue(5000, 3.0f, Result.NORMAL, interpolator);
+ verifyValue(7000, 4.0f, Result.FREEZE_END, interpolator);
+
+ // repeat once, no mirror
+ interpolator.setRepeatMirror(2, false);
+ verifyValue(1000, 4.0f, Result.FREEZE_END, interpolator); // known bug
+ verifyValue(3000, 1.5f, Result.NORMAL, interpolator);
+ verifyValue(5000, 3.0f, Result.NORMAL, interpolator);
+ verifyValue(7000, 1.5f, Result.NORMAL, interpolator);
+ verifyValue(9000, 3.0f, Result.NORMAL, interpolator);
+ verifyValue(11000, 4.0f, Result.FREEZE_END, interpolator);
+
+ // repeat once, mirror
+ interpolator.setRepeatMirror(2, true);
+ verifyValue(1000, 4.0f, Result.FREEZE_END, interpolator); // known bug
+ verifyValue(3000, 1.5f, Result.NORMAL, interpolator);
+ verifyValue(5000, 3.0f, Result.NORMAL, interpolator);
+ verifyValue(7000, 3.0f, Result.NORMAL, interpolator);
+ verifyValue(9000, 1.5f, Result.NORMAL, interpolator);
+ verifyValue(11000, 4.0f, Result.FREEZE_END, interpolator);
+ }
+
+ @Test
+ public void testSetKeyFrame() {
+ final float[] aZero = new float[] {0.0f};
+ final float[] aOne = new float[] {1.0f};
+
+ Interpolator interpolator = new Interpolator(1);
+ interpolator.setKeyFrame(0, 2000, aZero);
+ interpolator.setKeyFrame(1, 4000, aOne);
+ verifyValue(1000, 0.0f, Result.FREEZE_START, interpolator);
+ verifyValue(3000, 0.5f, Result.NORMAL, interpolator);
+ verifyValue(5000, 1.0f, Result.FREEZE_END, interpolator);
+
+ final float[] linearBlend = new float[] {0.0f, 0.0f, 1.0f, 1.0f};
+ final float[] accelerateBlend =
+ new float[] {
+ // approximate circle at PI/6 and PI/3
+ 0.5f, 1.0f - 0.866f, 0.866f, 0.5f
+ };
+ final float[] decelerateBlend =
+ new float[] {
+ // approximate circle at PI/6 and PI/3
+ 1.0f - 0.866f, 0.5f, 0.5f, 0.866f
+ };
+
+ // explicit linear blend should yield the same values
+ interpolator.setKeyFrame(0, 2000, aZero, linearBlend);
+ interpolator.setKeyFrame(1, 4000, aOne, linearBlend);
+ verifyValue(1000, 0.0f, Result.FREEZE_START, interpolator);
+ verifyValue(3000, 0.5f, Result.NORMAL, interpolator);
+ verifyValue(5000, 1.0f, Result.FREEZE_END, interpolator);
+
+ // blend of end key frame is not used
+ interpolator.setKeyFrame(0, 2000, aZero);
+ interpolator.setKeyFrame(1, 4000, aOne, accelerateBlend);
+ verifyValue(1000, 0.0f, Result.FREEZE_START, interpolator);
+ verifyValue(3000, 0.5f, Result.NORMAL, interpolator);
+ verifyValue(5000, 1.0f, Result.FREEZE_END, interpolator);
+
+ final float[] result = new float[1];
+
+ interpolator.setKeyFrame(0, 2000, aZero, accelerateBlend);
+ interpolator.setKeyFrame(1, 4000, aOne);
+ verifyValue(1000, 0.0f, Result.FREEZE_START, interpolator);
+ assertEquals(Result.NORMAL, interpolator.timeToValues(3000, result));
+ assertThat(result[0]).isLessThan(0.5f); // exact blend algorithm not known
+ verifyValue(5000, 1.0f, Result.FREEZE_END, interpolator);
+
+ interpolator.setKeyFrame(0, 2000, aZero, decelerateBlend);
+ interpolator.setKeyFrame(1, 4000, aOne);
+ verifyValue(1000, 0.0f, Result.FREEZE_START, interpolator);
+ assertEquals(Result.NORMAL, interpolator.timeToValues(3000, result));
+ assertThat(result[0]).isGreaterThan(0.5f); // exact blend algorithm not known
+ verifyValue(5000, 1.0f, Result.FREEZE_END, interpolator);
+
+ final int validTime = 0;
+ final int valueCount = 2;
+ interpolator.reset(valueCount);
+ assertEquals(valueCount, interpolator.getValueCount());
+ try {
+ // value array too short
+ interpolator.setKeyFrame(0, validTime, new float[valueCount - 1]);
+ fail("should throw ArrayStoreException");
+ } catch (ArrayStoreException e) {
+ // expected
+ }
+
+ try {
+ // index too small
+ interpolator.setKeyFrame(-1, validTime, new float[valueCount]);
+ fail("should throw IndexOutOfBoundsException");
+ } catch (IndexOutOfBoundsException e) {
+ // expected
+ }
+
+ try {
+ // index too large
+ interpolator.setKeyFrame(2, validTime, new float[valueCount]);
+ fail("should throw IndexOutOfBoundsException");
+ } catch (IndexOutOfBoundsException e) {
+ // expected
+ }
+
+ try {
+ // blend array too short
+ interpolator.setKeyFrame(0, validTime, new float[valueCount], new float[3]);
+ fail("should throw ArrayStoreException");
+ } catch (ArrayStoreException e) {
+ // expected
+ }
+ }
+
+ private void verifyValue(
+ int time, float expected, Result expectedResult, Interpolator interpolator) {
+ float[] values = new float[1];
+ assertEquals(expectedResult, interpolator.timeToValues(time, values));
+ assertEquals(expected, values[0], TOLERANCE);
+ }
+
+ private void verifyValues(
+ int time, float[] expected, Result expectedResult, Interpolator interpolator) {
+ float[] values = new float[expected.length];
+ assertEquals(expectedResult, interpolator.timeToValues(time, values));
+ assertArrayEquals(expected, values, TOLERANCE);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLightingColorFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLightingColorFilterTest.java
new file mode 100644
index 000000000..9d676334f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLightingColorFilterTest.java
@@ -0,0 +1,92 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LightingColorFilter;
+import android.graphics.Paint;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeLightingColorFilterTest {
+ private static final int TOLERANCE = 2;
+
+ private void verifyColor(int expected, int actual) {
+ ColorUtils.verifyColor(expected, actual, TOLERANCE);
+ }
+
+ @Test
+ public void testLightingColorFilter() {
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+
+ paint.setColor(Color.MAGENTA);
+ paint.setColorFilter(new LightingColorFilter(Color.WHITE, Color.BLACK));
+ canvas.drawPaint(paint);
+ verifyColor(Color.MAGENTA, bitmap.getPixel(0, 0));
+
+ paint.setColor(Color.MAGENTA);
+ paint.setColorFilter(new LightingColorFilter(Color.CYAN, Color.BLACK));
+ canvas.drawPaint(paint);
+ verifyColor(Color.BLUE, bitmap.getPixel(0, 0));
+
+ paint.setColor(Color.MAGENTA);
+ paint.setColorFilter(new LightingColorFilter(Color.BLUE, Color.GREEN));
+ canvas.drawPaint(paint);
+ verifyColor(Color.CYAN, bitmap.getPixel(0, 0));
+
+ // alpha is ignored
+ bitmap.eraseColor(Color.TRANSPARENT);
+ paint.setColor(Color.MAGENTA);
+ paint.setColorFilter(new LightingColorFilter(Color.TRANSPARENT, Color.argb(0, 0, 0xFF, 0)));
+ canvas.drawPaint(paint);
+ verifyColor(Color.GREEN, bitmap.getPixel(0, 0));
+
+ // channels get clipped (no overflow into green or alpha)
+ paint.setColor(Color.MAGENTA);
+ paint.setColorFilter(new LightingColorFilter(Color.WHITE, Color.MAGENTA));
+ canvas.drawPaint(paint);
+ verifyColor(Color.MAGENTA, bitmap.getPixel(0, 0));
+
+ // multiply before add
+ paint.setColor(Color.argb(255, 60, 20, 40));
+ paint.setColorFilter(
+ new LightingColorFilter(Color.rgb(0x80, 0xFF, 0x80), Color.rgb(0, 10, 10)));
+ canvas.drawPaint(paint);
+ verifyColor(Color.argb(255, 30, 30, 30), bitmap.getPixel(0, 0));
+
+ // source alpha remains unchanged
+ bitmap.eraseColor(Color.TRANSPARENT);
+ paint.setColor(Color.argb(0x80, 60, 20, 40));
+ paint.setColorFilter(
+ new LightingColorFilter(Color.rgb(0x80, 0xFF, 0x80), Color.rgb(0, 10, 10)));
+ canvas.drawPaint(paint);
+ verifyColor(Color.argb(0x80, 30, 30, 30), bitmap.getPixel(0, 0));
+ }
+
+ @Test
+ public void testGetColorAdd() {
+ LightingColorFilter filter = new LightingColorFilter(Color.WHITE, Color.BLACK);
+ ColorUtils.verifyColor(Color.BLACK, filter.getColorAdd());
+
+ filter = new LightingColorFilter(0x87654321, 0x12345678);
+ ColorUtils.verifyColor(0x12345678, filter.getColorAdd());
+ }
+
+ @Test
+ public void testGetColorMultiply() {
+ LightingColorFilter filter = new LightingColorFilter(Color.WHITE, Color.BLACK);
+ ColorUtils.verifyColor(Color.WHITE, filter.getColorMultiply());
+
+ filter = new LightingColorFilter(0x87654321, 0x12345678);
+ ColorUtils.verifyColor(0x87654321, filter.getColorMultiply());
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLineBreakerTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLineBreakerTest.java
new file mode 100644
index 000000000..74aa37a41
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLineBreakerTest.java
@@ -0,0 +1,116 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED;
+import static android.graphics.text.LineBreaker.BREAK_STRATEGY_HIGH_QUALITY;
+import static android.graphics.text.LineBreaker.BREAK_STRATEGY_SIMPLE;
+import static android.graphics.text.LineBreaker.HYPHENATION_FREQUENCY_FULL;
+import static android.graphics.text.LineBreaker.HYPHENATION_FREQUENCY_NONE;
+import static android.graphics.text.LineBreaker.HYPHENATION_FREQUENCY_NORMAL;
+import static android.graphics.text.LineBreaker.JUSTIFICATION_MODE_INTER_WORD;
+import static android.graphics.text.LineBreaker.JUSTIFICATION_MODE_NONE;
+import static android.os.Build.VERSION_CODES.Q;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.text.LineBreaker;
+import android.graphics.text.LineBreaker.ParagraphConstraints;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = Q)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeLineBreakerTest {
+
+ private static final String TAG = "LineBreakerTest";
+
+ private static Paint paint;
+
+ @Before
+ public void setup() {
+ paint = new Paint();
+ Context context = RuntimeEnvironment.getApplication();
+ AssetManager am = context.getAssets();
+ Typeface tf = new Typeface.Builder(am, "fonts/layout/linebreak.ttf").build();
+ paint.setTypeface(tf);
+ paint.setTextSize(10.0f); // Make 1em = 10px
+ }
+
+ @Test
+ public void testLineBreak_construct() {
+ assertNotNull(new LineBreaker.Builder().build());
+ }
+
+ @Test
+ public void testSetBreakStrategy_shouldNotThrowExceptions() {
+ assertNotNull(new LineBreaker.Builder().setBreakStrategy(BREAK_STRATEGY_SIMPLE).build());
+ assertNotNull(new LineBreaker.Builder().setBreakStrategy(BREAK_STRATEGY_HIGH_QUALITY).build());
+ assertNotNull(new LineBreaker.Builder().setBreakStrategy(BREAK_STRATEGY_BALANCED).build());
+ }
+
+ @Test
+ public void testSetHyphenationFrequency_shouldNotThrowExceptions() {
+ assertNotNull(
+ new LineBreaker.Builder().setHyphenationFrequency(HYPHENATION_FREQUENCY_NORMAL).build());
+ assertNotNull(
+ new LineBreaker.Builder().setHyphenationFrequency(HYPHENATION_FREQUENCY_FULL).build());
+ assertNotNull(
+ new LineBreaker.Builder().setHyphenationFrequency(HYPHENATION_FREQUENCY_NONE).build());
+ }
+
+ @Test
+ public void testSetJustification_shouldNotThrowExceptions() {
+ assertNotNull(new LineBreaker.Builder().setJustificationMode(JUSTIFICATION_MODE_NONE).build());
+ assertNotNull(
+ new LineBreaker.Builder().setJustificationMode(JUSTIFICATION_MODE_INTER_WORD).build());
+ }
+
+ @Test
+ public void testSetIntent_shouldNotThrowExceptions() {
+ assertNotNull(new LineBreaker.Builder().setIndents(null).build());
+ assertNotNull(new LineBreaker.Builder().setIndents(new int[] {}).build());
+ assertNotNull(new LineBreaker.Builder().setIndents(new int[] {100}).build());
+ }
+
+ @Test
+ public void testSetGetWidth() {
+ ParagraphConstraints c = new ParagraphConstraints();
+ assertEquals(0, c.getWidth(), 0.0f); // 0 by default
+ c.setWidth(100);
+ assertEquals(100, c.getWidth(), 0.0f);
+ c.setWidth(200);
+ assertEquals(200, c.getWidth(), 0.0f);
+ }
+
+ @Test
+ public void testSetGetIndent() {
+ ParagraphConstraints c = new ParagraphConstraints();
+ assertEquals(0.0f, c.getFirstWidth(), 0.0f); // 0 by default
+ assertEquals(0, c.getFirstWidthLineCount()); // 0 by default
+ c.setIndent(100.0f, 1);
+ assertEquals(100.0f, c.getFirstWidth(), 0.0f);
+ assertEquals(1, c.getFirstWidthLineCount());
+ c.setIndent(200.0f, 5);
+ assertEquals(200.0f, c.getFirstWidth(), 0.0f);
+ assertEquals(5, c.getFirstWidthLineCount());
+ }
+
+ @Test
+ public void testSetGetTabStops() {
+ ParagraphConstraints c = new ParagraphConstraints();
+ assertNull(c.getTabStops()); // null by default
+ assertEquals(0, c.getDefaultTabStop(), 0.0); // 0 by default
+ c.setTabStops(new float[] {120}, 240);
+ assertEquals(1, c.getTabStops().length);
+ assertEquals(120, c.getTabStops()[0], 0.0);
+ assertEquals(240, c.getDefaultTabStop(), 0.0);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLinearGradientTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLinearGradientTest.java
new file mode 100644
index 000000000..c0488fd1d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeLinearGradientTest.java
@@ -0,0 +1,301 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorSpace;
+import android.graphics.LinearGradient;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Shader.TileMode;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.function.Function;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeLinearGradientTest {
+
+ @Test
+ public void testLinearGradient() {
+ Bitmap b;
+ LinearGradient lg;
+ int[] color = {Color.BLUE, Color.GREEN, Color.RED};
+ float[] position = {0.0f, 1.0f / 3.0f, 2.0f / 3.0f};
+
+ lg = new LinearGradient(0, 0, 0, 40, color, position, TileMode.CLAMP);
+ b = drawLinearGradient(lg, Bitmap.Config.ARGB_8888);
+
+ // The pixels in same gradient line should be equivalent
+ assertEquals(b.getPixel(10, 10), b.getPixel(20, 10));
+ // BLUE -> GREEN, B sub-value decreasing while G sub-value increasing
+ assertTrue(Color.blue(b.getPixel(10, 0)) > Color.blue(b.getPixel(10, 5)));
+ assertTrue(Color.blue(b.getPixel(10, 5)) > Color.blue(b.getPixel(10, 10)));
+ assertTrue(Color.green(b.getPixel(10, 0)) < Color.green(b.getPixel(10, 5)));
+ assertTrue(Color.green(b.getPixel(10, 5)) < Color.green(b.getPixel(10, 10)));
+ // GREEN -> RED, G sub-value decreasing while R sub-value increasing
+ assertTrue(Color.green(b.getPixel(10, 15)) > Color.green(b.getPixel(10, 20)));
+ assertTrue(Color.green(b.getPixel(10, 20)) > Color.green(b.getPixel(10, 25)));
+ assertTrue(Color.red(b.getPixel(10, 15)) < Color.red(b.getPixel(10, 20)));
+ assertTrue(Color.red(b.getPixel(10, 20)) < Color.red(b.getPixel(10, 25)));
+
+ lg = new LinearGradient(0, 0, 0, 40, Color.RED, Color.BLUE, TileMode.CLAMP);
+ b = drawLinearGradient(lg, Bitmap.Config.ARGB_8888);
+
+ // The pixels in same gradient line should be equivalent
+ assertEquals(b.getPixel(10, 10), b.getPixel(20, 10));
+ // RED -> BLUE, R sub-value decreasing while B sub-value increasing
+ assertTrue(Color.red(b.getPixel(10, 0)) > Color.red(b.getPixel(10, 15)));
+ assertTrue(Color.red(b.getPixel(10, 15)) > Color.red(b.getPixel(10, 30)));
+ assertTrue(Color.blue(b.getPixel(10, 0)) < Color.blue(b.getPixel(10, 15)));
+ assertTrue(Color.blue(b.getPixel(10, 15)) < Color.blue(b.getPixel(10, 30)));
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testLinearGradientLong() {
+ ColorSpace p3 = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
+ long red = Color.pack(1, 0, 0, 1, p3);
+ long green = Color.pack(0, 1, 0, 1, p3);
+ long blue = Color.pack(0, 0, 1, 1, p3);
+ long[] colors = new long[] {blue, green, red};
+ float[] positions = null;
+
+ LinearGradient lg = new LinearGradient(0, 0, 0, 40, colors, positions, TileMode.CLAMP);
+ Bitmap b = drawLinearGradient(lg, Bitmap.Config.RGBA_F16);
+ final ColorSpace bitmapColorSpace = b.getColorSpace();
+ Function<Long, Color> convert =
+ (l) -> {
+ return Color.valueOf(Color.convert(l, bitmapColorSpace));
+ };
+
+ ColorUtils.verifyColor(
+ "Top-most color should be mostly blue!", convert.apply(blue), b.getColor(0, 0), 0.09f);
+
+ ColorUtils.verifyColor(
+ "Middle color should be mostly green!", convert.apply(green), b.getColor(0, 20), 0.09f);
+
+ ColorUtils.verifyColor(
+ "Bottom-most color should be mostly red!", convert.apply(red), b.getColor(0, 39), 0.08f);
+
+ ColorUtils.verifyColor(
+ "The pixels in same gradient line should be equivalent!",
+ b.getColor(10, 10),
+ b.getColor(20, 10),
+ 0f);
+ // BLUE -> GREEN, B sub-value decreasing while G sub-value increasing
+ assertTrue(b.getColor(10, 0).blue() > b.getColor(10, 5).blue());
+ assertTrue(b.getColor(10, 5).blue() > b.getColor(10, 10).blue());
+ assertTrue(b.getColor(10, 0).green() < b.getColor(10, 5).green());
+ assertTrue(b.getColor(10, 5).green() < b.getColor(10, 10).green());
+ // GREEN -> RED, G sub-value decreasing while R sub-value increasing
+ assertTrue(b.getColor(10, 20).green() > b.getColor(10, 30).green());
+ assertTrue(b.getColor(10, 30).green() > b.getColor(10, 35).green());
+ assertTrue(b.getColor(10, 20).red() < b.getColor(10, 30).red());
+ assertTrue(b.getColor(10, 30).red() < b.getColor(10, 35).red());
+
+ lg = new LinearGradient(0, 0, 0, 40, red, blue, TileMode.CLAMP);
+ b = drawLinearGradient(lg, Bitmap.Config.RGBA_F16);
+
+ ColorUtils.verifyColor(
+ "Top-most color should be mostly red!", convert.apply(red), b.getColor(0, 0), .03f);
+
+ ColorUtils.verifyColor(
+ "Bottom-most color should be mostly blue!", convert.apply(blue), b.getColor(0, 39), 0.016f);
+
+ ColorUtils.verifyColor(
+ "The pixels in same gradient line should be equivalent!",
+ b.getColor(10, 10),
+ b.getColor(20, 10),
+ 0f);
+ // RED -> BLUE, R sub-value decreasing while B sub-value increasing
+ assertTrue(b.getColor(10, 0).red() > b.getColor(10, 15).red());
+ assertTrue(b.getColor(10, 15).red() > b.getColor(10, 30).red());
+ assertTrue(b.getColor(10, 0).blue() < b.getColor(10, 15).blue());
+ assertTrue(b.getColor(10, 15).blue() < b.getColor(10, 30).blue());
+ }
+
+ private Bitmap drawLinearGradient(LinearGradient lg, Bitmap.Config c) {
+ Paint paint = new Paint();
+ paint.setShader(lg);
+ Bitmap b = Bitmap.createBitmap(40, 40, c);
+ b.eraseColor(Color.BLACK);
+ Canvas canvas = new Canvas(b);
+ canvas.drawPaint(paint);
+ return b;
+ }
+
+ @Test
+ public void testZeroScaleMatrix() {
+ LinearGradient gradient =
+ new LinearGradient(0.5f, 0, 1.5f, 0, Color.RED, Color.BLUE, TileMode.CLAMP);
+ Matrix m = new Matrix();
+ m.setScale(0, 0);
+ gradient.setLocalMatrix(m);
+ Bitmap bitmap = drawLinearGradient(gradient, Bitmap.Config.ARGB_8888);
+
+ ColorUtils.verifyColor(Color.BLACK, bitmap.getPixel(0, 0), 1);
+ ColorUtils.verifyColor(Color.BLACK, bitmap.getPixel(20, 20), 1);
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testNullColorInts() {
+ int[] colors = null;
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, colors, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testNullColorLongs() {
+ long[] colors = null;
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, colors, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testNoColorInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, new int[0], null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testNoColorLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, new long[0], null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testOneColorInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, new int[1], null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testOneColorLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, new long[1], null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchColorLongs() {
+ long[] colors = new long[2];
+ colors[0] = Color.pack(Color.BLUE);
+ colors[1] = Color.pack(.5f, .5f, .5f, 1.0f, ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, colors, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchColorLongs2() {
+ long color0 = Color.pack(Color.BLUE);
+ long color1 = Color.pack(.5f, .5f, .5f, 1.0f, ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, color0, color1, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchPositionsInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, new int[2], new float[3], TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchPositionsLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, new long[2], new float[3], TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLongs() {
+ long[] colors = new long[2];
+ colors[0] = -1L;
+ colors[0] = -2L;
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, colors, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLong() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, -1L, Color.pack(Color.RED), TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLong2() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ LinearGradient unused =
+ new LinearGradient(0.5f, 0, 1.5f, 0, Color.pack(Color.RED), -1L, TileMode.CLAMP);
+ });
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMaskFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMaskFilterTest.java
new file mode 100644
index 000000000..a0174f044
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMaskFilterTest.java
@@ -0,0 +1,18 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.MaskFilter;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeMaskFilterTest {
+ @Test
+ public void testConstructor() {
+ var unused = new MaskFilter();
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMatrixTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMatrixTest.java
new file mode 100644
index 000000000..a7bd3b624
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMatrixTest.java
@@ -0,0 +1,445 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.RectF;
+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;
+import org.robolectric.shadows.ShadowMatrix;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeMatrixTest {
+ private static final float EPSILON = 1e-7f;
+
+ @Test
+ public void testIsIdentity() {
+ final Matrix matrix = new Matrix();
+ assertThat(matrix.isIdentity()).isTrue();
+
+ matrix.postScale(2.0f, 2.0f);
+ assertThat(matrix.isIdentity()).isFalse();
+ }
+
+ @Test
+ public void testIsAffine() {
+ final Matrix matrix = new Matrix();
+ assertThat(matrix.isAffine()).isTrue();
+
+ matrix.postScale(2.0f, 2.0f);
+ assertThat(matrix.isAffine()).isTrue();
+ matrix.postTranslate(1.0f, 2.0f);
+ assertThat(matrix.isAffine()).isTrue();
+ matrix.postRotate(45.0f);
+ assertThat(matrix.isAffine()).isTrue();
+
+ matrix.setValues(new float[] {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 2.0f});
+ assertThat(matrix.isAffine()).isFalse();
+ }
+
+ @Test
+ public void testRectStaysRect() {
+ final Matrix matrix = new Matrix();
+ assertThat(matrix.rectStaysRect()).isTrue();
+
+ matrix.postScale(2.0f, 2.0f);
+ assertThat(matrix.rectStaysRect()).isTrue();
+ matrix.postTranslate(1.0f, 2.0f);
+ assertThat(matrix.rectStaysRect()).isTrue();
+ matrix.postRotate(45.0f);
+ assertThat(matrix.rectStaysRect()).isFalse();
+ matrix.postRotate(45.0f);
+ assertThat(matrix.rectStaysRect()).isTrue();
+ }
+
+ @Test
+ public void testGetSetValues() {
+ final Matrix matrix = new Matrix();
+ final float[] values = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
+ matrix.setValues(values);
+ final float[] matrixValues = new float[9];
+ matrix.getValues(matrixValues);
+ assertThat(matrixValues).isEqualTo(values);
+ }
+
+ @Test
+ public void testSet() {
+ final Matrix matrix1 = new Matrix();
+ matrix1.postScale(2.0f, 2.0f);
+ matrix1.postTranslate(1.0f, 2.0f);
+ matrix1.postRotate(45.0f);
+
+ final Matrix matrix2 = new Matrix();
+ matrix2.set(matrix1);
+ assertThat(matrix1).isEqualTo(matrix2);
+
+ matrix2.set(null);
+ assertThat(matrix2.isIdentity()).isTrue();
+ }
+
+ @Test
+ public void testReset() {
+ final Matrix matrix = new Matrix();
+ matrix.postScale(2.0f, 2.0f);
+ matrix.postTranslate(1.0f, 2.0f);
+ matrix.postRotate(45.0f);
+ matrix.reset();
+ assertThat(matrix.isIdentity()).isTrue();
+ }
+
+ @Test
+ public void testSetTranslate() {
+ final Matrix matrix = new Matrix();
+ matrix.setTranslate(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix, 1.0f, 1.0f), new PointF(3.0f, 3.0f));
+ matrix.setTranslate(-2.0f, -2.0f);
+ assertPointsEqual(mapPoint(matrix, 1.0f, 1.0f), new PointF(-1.0f, -1.0f));
+ }
+
+ @Test
+ public void testPostTranslate() {
+ final Matrix matrix1 = new Matrix();
+ matrix1.postTranslate(1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(2.0f, 2.0f));
+
+ matrix1.postTranslate(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(4.0f, 4.0f));
+
+ final Matrix matrix2 = new Matrix();
+ matrix2.setScale(2.0f, 2.0f);
+ matrix2.postTranslate(-5.0f, 10.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(-3.0f, 12.0f));
+ }
+
+ @Test
+ public void testPreTranslate() {
+ final Matrix matrix1 = new Matrix();
+ matrix1.preTranslate(1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(2.0f, 2.0f));
+
+ matrix1.preTranslate(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(4.0f, 4.0f));
+
+ final Matrix matrix2 = new Matrix();
+ matrix2.setScale(2.0f, 2.0f);
+ matrix2.preTranslate(-5.0f, 10.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(-8.0f, 22.0f));
+ }
+
+ @Test
+ public void testSetScale() {
+ final Matrix matrix = new Matrix();
+ matrix.setScale(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix, 1.0f, 1.0f), new PointF(2.0f, 2.0f));
+ matrix.setScale(-2.0f, -3.0f);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 3.0f), new PointF(-4.0f, -9.0f));
+ matrix.setScale(-2.0f, -3.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 3.0f), new PointF(-1.0f, -5.0f));
+ }
+
+ @Test
+ public void testPostScale() {
+ final Matrix matrix1 = new Matrix();
+ matrix1.postScale(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(2.0f, 2.0f));
+
+ matrix1.postScale(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(4.0f, 4.0f));
+
+ final Matrix matrix2 = new Matrix();
+ matrix2.postScale(2.0f, 2.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(1.0f, 1.0f));
+
+ matrix2.setTranslate(1.0f, 2.0f);
+ matrix2.postScale(2.0f, 2.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(3.0f, 5.0f));
+ }
+
+ @Test
+ public void testPreScale() {
+ final Matrix matrix1 = new Matrix();
+ matrix1.preScale(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(2.0f, 2.0f));
+
+ matrix1.preScale(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(4.0f, 4.0f));
+
+ final Matrix matrix2 = new Matrix();
+ matrix2.preScale(2.0f, 2.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(1.0f, 1.0f));
+
+ matrix2.setTranslate(1.0f, 2.0f);
+ matrix2.preScale(2.0f, 2.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(2.0f, 3.0f));
+ }
+
+ @Test
+ public void testSetRotate() {
+ final Matrix matrix = new Matrix();
+ matrix.setRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(-1.0f, 0.0f));
+ matrix.setRotate(180.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, -1.0f));
+ matrix.setRotate(270.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(1.0f, 0.0f));
+ matrix.setRotate(360.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, 1.0f));
+
+ matrix.setRotate(45.0f, 0.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, 1.0f));
+ }
+
+ @Test
+ public void testPostRotate() {
+ final Matrix matrix = new Matrix();
+ matrix.postRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(-1.0f, 0.0f));
+ matrix.postRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, -1.0f));
+ matrix.postRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(1.0f, 0.0f));
+ matrix.postRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, 1.0f));
+
+ matrix.setTranslate(1.0f, 2.0f);
+ matrix.postRotate(45.0f, 0.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(-0.70710677f, 3.1213202f));
+ }
+
+ @Test
+ public void testPreRotate() {
+ final Matrix matrix = new Matrix();
+ matrix.preRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(-1.0f, 0.0f));
+ matrix.preRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, -1.0f));
+ matrix.preRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(1.0f, 0.0f));
+ matrix.preRotate(90.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, 1.0f));
+
+ matrix.setTranslate(1.0f, 2.0f);
+ matrix.preRotate(45.0f, 0.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(1.0f, 3.0f));
+ }
+
+ @Test
+ public void testSetSinCos() {
+ final Matrix matrix = new Matrix();
+ matrix.setSinCos(1.0f, 0.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(-1.0f, 0.0f));
+ matrix.setSinCos(0.0f, -1.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, -1.0f));
+ matrix.setSinCos(-1.0f, 0.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(1.0f, 0.0f));
+ matrix.setSinCos(0.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, 1.0f));
+
+ final float sinCos = (float) Math.sqrt(2) / 2;
+ matrix.setSinCos(sinCos, sinCos, 0.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix, 0.0f, 1.0f), new PointF(0.0f, 1.0f));
+ }
+
+ @Test
+ public void testSetSkew() {
+ final Matrix matrix = new Matrix();
+ matrix.setSkew(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix, 1.0f, 1.0f), new PointF(3.0f, 3.0f));
+ matrix.setSkew(-2.0f, -3.0f);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 3.0f), new PointF(-4.0f, -3.0f));
+ matrix.setSkew(-2.0f, -3.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 3.0f), new PointF(-2.0f, 0.0f));
+ }
+
+ @Test
+ public void testPostSkew() {
+ final Matrix matrix1 = new Matrix();
+ matrix1.postSkew(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(3.0f, 3.0f));
+
+ matrix1.postSkew(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(9.0f, 9.0f));
+
+ final Matrix matrix2 = new Matrix();
+ matrix2.postSkew(2.0f, 2.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(1.0f, 1.0f));
+
+ matrix2.setTranslate(1.0f, 2.0f);
+ matrix2.postSkew(2.0f, 2.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(6.0f, 5.0f));
+ }
+
+ @Test
+ public void testPreSkew() {
+ final Matrix matrix1 = new Matrix();
+ matrix1.preSkew(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(3.0f, 3.0f));
+
+ matrix1.preSkew(2.0f, 2.0f);
+ assertPointsEqual(mapPoint(matrix1, 1.0f, 1.0f), new PointF(9.0f, 9.0f));
+
+ final Matrix matrix2 = new Matrix();
+ matrix2.preSkew(2.0f, 2.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(1.0f, 1.0f));
+
+ matrix2.setTranslate(1.0f, 2.0f);
+ matrix2.preSkew(2.0f, 2.0f, 1.0f, 1.0f);
+ assertPointsEqual(mapPoint(matrix2, 1.0f, 1.0f), new PointF(2.0f, 3.0f));
+ }
+
+ @Test
+ public void testSetConcat() {
+ final Matrix scaleMatrix = new Matrix();
+ scaleMatrix.setScale(2.0f, 3.0f);
+ final Matrix translateMatrix = new Matrix();
+ translateMatrix.postTranslate(5.0f, 7.0f);
+ final Matrix matrix = new Matrix();
+ matrix.setConcat(translateMatrix, scaleMatrix);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 2.0f), new PointF(9.0f, 13.0f));
+
+ final Matrix rotateMatrix = new Matrix();
+ rotateMatrix.postRotate(90.0f);
+ matrix.setConcat(rotateMatrix, matrix);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 2.0f), new PointF(-13.0f, 9.0f));
+ }
+
+ @Test
+ public void testPostConcat() {
+ final Matrix matrix = new Matrix();
+ matrix.postScale(2.0f, 3.0f);
+ final Matrix translateMatrix = new Matrix();
+ translateMatrix.postTranslate(5.0f, 7.0f);
+ matrix.postConcat(translateMatrix);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 2.0f), new PointF(9.0f, 13.0f));
+
+ final Matrix rotateMatrix = new Matrix();
+ rotateMatrix.postRotate(90.0f);
+ matrix.postConcat(rotateMatrix);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 2.0f), new PointF(-13.0f, 9.0f));
+ }
+
+ @Test
+ public void testPreConcat() {
+ final Matrix matrix = new Matrix();
+ matrix.preScale(2.0f, 3.0f);
+ final Matrix translateMatrix = new Matrix();
+ translateMatrix.setTranslate(5.0f, 7.0f);
+ matrix.preConcat(translateMatrix);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 2.0f), new PointF(14.0f, 27.0f));
+
+ final Matrix rotateMatrix = new Matrix();
+ rotateMatrix.setRotate(90.0f);
+ matrix.preConcat(rotateMatrix);
+ assertPointsEqual(mapPoint(matrix, 2.0f, 2.0f), new PointF(6.0f, 27.0f));
+ }
+
+ @Test
+ public void testInvert() {
+ final Matrix matrix = new Matrix();
+ final Matrix inverse = new Matrix();
+ matrix.setScale(0.0f, 1.0f);
+ assertThat(matrix.invert(inverse)).isFalse();
+ matrix.setScale(1.0f, 0.0f);
+ assertThat(matrix.invert(inverse)).isFalse();
+
+ matrix.setScale(1.0f, 1.0f);
+ checkInverse(matrix);
+ matrix.setScale(-3.0f, 5.0f);
+ checkInverse(matrix);
+ matrix.setTranslate(5.0f, 2.0f);
+ checkInverse(matrix);
+ matrix.setScale(-3.0f, 5.0f);
+ matrix.postTranslate(5.0f, 2.0f);
+ checkInverse(matrix);
+ matrix.setScale(-3.0f, 5.0f);
+ matrix.postRotate(-30f, 1.0f, 2.0f);
+ matrix.postTranslate(5.0f, 2.0f);
+ checkInverse(matrix);
+ }
+
+ @Test
+ public void testMapRect() {
+ final Matrix matrix = new Matrix();
+ matrix.postScale(2.0f, 3.0f);
+ final RectF input = new RectF(1.0f, 1.0f, 2.0f, 2.0f);
+ final RectF output1 = new RectF();
+ matrix.mapRect(output1, input);
+ assertThat(output1).isEqualTo(new RectF(2.0f, 3.0f, 4.0f, 6.0f));
+
+ matrix.postScale(-1.0f, -1.0f);
+ final RectF output2 = new RectF();
+ matrix.mapRect(output2, input);
+ assertThat(output2).isEqualTo(new RectF(-4.0f, -6.0f, -2.0f, -3.0f));
+ }
+
+ @Test
+ public void testMapPoints() {
+ final Matrix matrix = new Matrix();
+ matrix.postTranslate(-1.0f, -2.0f);
+ matrix.postScale(2.0f, 3.0f);
+ final float[] input = {
+ 0.0f, 0.0f,
+ 1.0f, 2.0f
+ };
+ final float[] output = new float[input.length];
+ matrix.mapPoints(output, input);
+ assertThat(output).usingExactEquality().containsExactly(-2.0f, -6.0f, 0.0f, 0.0f);
+ }
+
+ @Test
+ public void testMapVectors() {
+ final Matrix matrix = new Matrix();
+ matrix.postTranslate(-1.0f, -2.0f);
+ matrix.postScale(2.0f, 3.0f);
+ final float[] input = {
+ 0.0f, 0.0f,
+ 1.0f, 2.0f
+ };
+ final float[] output = new float[input.length];
+ matrix.mapVectors(output, input);
+ assertThat(output).usingExactEquality().containsExactly(0.0f, 0.0f, 2.0f, 6.0f);
+ }
+
+ @Test
+ public void legacyShadowPathAPIs_notSupported() {
+ Matrix matrix = new Matrix();
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> ((ShadowMatrix) Shadow.extract(matrix)).getDescription());
+ }
+
+ private static PointF mapPoint(Matrix matrix, float x, float y) {
+ float[] pf = new float[] {x, y};
+ matrix.mapPoints(pf);
+ return new PointF(pf[0], pf[1]);
+ }
+
+ private static void assertPointsEqual(PointF actual, PointF expected) {
+ assertThat(actual.x).isWithin(EPSILON).of(expected.x);
+ assertThat(actual.y).isWithin(EPSILON).of(expected.y);
+ }
+
+ private static void checkInverse(Matrix matrix) {
+ final Matrix inverse = new Matrix();
+ assertThat(matrix.invert(inverse)).isTrue();
+ matrix.postConcat(inverse);
+ float[] vals = new float[9];
+ matrix.getValues(vals);
+ assertThat(vals[0]).isWithin(EPSILON).of(1);
+ assertThat(vals[1]).isWithin(EPSILON).of(0);
+ assertThat(vals[2]).isWithin(EPSILON).of(0);
+ assertThat(vals[3]).isWithin(EPSILON).of(0);
+ assertThat(vals[4]).isWithin(EPSILON).of(1);
+ assertThat(vals[5]).isWithin(EPSILON).of(0);
+ assertThat(vals[6]).isWithin(EPSILON).of(0);
+ assertThat(vals[7]).isWithin(EPSILON).of(0);
+ assertThat(vals[8]).isWithin(EPSILON).of(1);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMeasuredParagraphTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMeasuredParagraphTest.java
new file mode 100644
index 000000000..5d2c9227f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMeasuredParagraphTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.P;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.text.Layout;
+import android.text.MeasuredParagraph;
+import android.text.TextDirectionHeuristic;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = P)
+public class ShadowNativeMeasuredParagraphTest {
+ private static final TextDirectionHeuristic LTR = TextDirectionHeuristics.LTR;
+ private static final TextDirectionHeuristic RTL = TextDirectionHeuristics.RTL;
+
+ private final TextPaint paint = new TextPaint();
+
+ @Before
+ public void setUp() { // The test font has following coverage and width.
+ // U+0020: 10em
+ // U+002E (.): 10em
+ // U+0043 (C): 100em
+ // U+0049 (I): 1em
+ // U+004C (L): 50em
+ // U+0056 (V): 5em
+ // U+0058 (X): 10em
+ // U+005F (_): 0em
+ // U+FFFD (invalid surrogate will be replaced to this): 7em
+ // U+10331 (\uD800\uDF31): 10em
+ Context context = RuntimeEnvironment.getApplication();
+ paint.setTypeface(
+ Typeface.createFromAsset(
+ context.getAssets(), "fonts/StaticLayoutLineBreakingTestFont.ttf"));
+ paint.setTextSize(1.0f); // Make 1em == 1px.
+ }
+
+ private String charsToString(char[] chars) {
+ return String.valueOf(chars);
+ }
+
+ @Test
+ public void buildForBidi() {
+
+ MeasuredParagraph mt = MeasuredParagraph.buildForBidi("XXX", 0, 3, LTR, null);
+ assertNotNull(mt);
+ assertNotNull(mt.getChars());
+ assertEquals("XXX", charsToString(mt.getChars()));
+ assertEquals(Layout.DIR_LEFT_TO_RIGHT, mt.getParagraphDir());
+ assertNotNull(mt.getDirections(0, 3));
+ assertEquals(0, mt.getWholeWidth(), 0);
+ assertEquals(0, mt.getWidths().size());
+ assertEquals(0, mt.getSpanEndCache().size());
+ assertEquals(0, mt.getFontMetrics().size());
+ assertEquals(0, reflector(MeasuredParagraphReflector.class, mt).getNativePtr());
+
+ // Recycle it
+ MeasuredParagraph mt2 = MeasuredParagraph.buildForBidi("_VVV_", 1, 4, RTL, mt);
+ assertEquals(mt2, mt);
+ assertNotNull(mt2.getChars());
+ assertEquals("VVV", charsToString(mt.getChars()));
+ assertNotNull(mt2.getDirections(0, 3));
+ assertEquals(0, mt2.getWholeWidth(), 0);
+ assertEquals(0, mt2.getWidths().size());
+ assertEquals(0, mt2.getSpanEndCache().size());
+ assertEquals(0, mt2.getFontMetrics().size());
+ assertEquals(0, reflector(MeasuredParagraphReflector.class, mt2).getNativePtr());
+
+ mt2.recycle();
+ }
+
+ @Test
+ public void buildForMeasurement() {
+
+ MeasuredParagraph mt = MeasuredParagraph.buildForMeasurement(paint, "XXX", 0, 3, LTR, null);
+ assertNotNull(mt);
+ assertNotNull(mt.getChars());
+ assertEquals("XXX", charsToString(mt.getChars()));
+ assertEquals(Layout.DIR_LEFT_TO_RIGHT, mt.getParagraphDir());
+ assertNotNull(mt.getDirections(0, 3));
+ assertEquals(30, mt.getWholeWidth(), 0);
+ assertEquals(3, mt.getWidths().size());
+ assertEquals(10, mt.getWidths().get(0), 0);
+ assertEquals(10, mt.getWidths().get(1), 0);
+ assertEquals(10, mt.getWidths().get(2), 0);
+ assertEquals(0, mt.getSpanEndCache().size());
+ assertEquals(0, mt.getFontMetrics().size());
+ assertEquals(0, reflector(MeasuredParagraphReflector.class, mt).getNativePtr());
+
+ // Recycle it
+ MeasuredParagraph mt2 = MeasuredParagraph.buildForMeasurement(paint, "_VVV_", 1, 4, RTL, mt);
+ assertEquals(mt2, mt);
+ assertNotNull(mt2.getChars());
+ assertEquals("VVV", charsToString(mt.getChars()));
+ assertEquals(Layout.DIR_RIGHT_TO_LEFT, mt2.getParagraphDir());
+ assertNotNull(mt2.getDirections(0, 3));
+ assertEquals(15, mt2.getWholeWidth(), 0);
+ assertEquals(3, mt2.getWidths().size());
+ assertEquals(5, mt2.getWidths().get(0), 0);
+ assertEquals(5, mt2.getWidths().get(1), 0);
+ assertEquals(5, mt2.getWidths().get(2), 0);
+ assertEquals(0, mt2.getSpanEndCache().size());
+ assertEquals(0, mt2.getFontMetrics().size());
+ assertEquals(0, reflector(MeasuredParagraphReflector.class, mt2).getNativePtr());
+
+ mt2.recycle();
+ }
+
+ @Test
+ public void buildForStaticLayout() {
+
+ MeasuredParagraph mt =
+ (MeasuredParagraph)
+ reflector(MeasuredParagraphReflector.class)
+ .buildForStaticLayout(paint, "XXX", 0, 3, LTR, false, false, null);
+ assertNotNull(mt);
+ assertNotNull(mt.getChars());
+ assertEquals("XXX", charsToString(mt.getChars()));
+ assertEquals(Layout.DIR_LEFT_TO_RIGHT, mt.getParagraphDir());
+ assertNotNull(mt.getDirections(0, 3));
+ assertEquals(0, mt.getWholeWidth(), 0);
+ assertEquals(0, mt.getWidths().size());
+ assertEquals(1, mt.getSpanEndCache().size());
+ assertEquals(3, mt.getSpanEndCache().get(0));
+ assertNotEquals(0, mt.getFontMetrics().size());
+ assertNotEquals(0, reflector(MeasuredParagraphReflector.class, mt).getNativePtr());
+
+ // Recycle it
+ MeasuredParagraph mt2 =
+ (MeasuredParagraph)
+ reflector(MeasuredParagraphReflector.class)
+ .buildForStaticLayout(paint, "_VVV_", 1, 4, RTL, false, false, mt);
+ assertEquals(mt2, mt);
+ assertNotNull(mt2.getChars());
+ assertEquals("VVV", charsToString(mt.getChars()));
+ assertEquals(Layout.DIR_RIGHT_TO_LEFT, mt2.getParagraphDir());
+ assertNotNull(mt2.getDirections(0, 3));
+ assertEquals(0, mt2.getWholeWidth(), 0);
+ assertEquals(0, mt2.getWidths().size());
+ assertEquals(1, mt2.getSpanEndCache().size());
+ assertEquals(4, mt2.getSpanEndCache().get(0));
+ assertNotEquals(0, mt2.getFontMetrics().size());
+ assertNotEquals(0, reflector(MeasuredParagraphReflector.class, mt2).getNativePtr());
+
+ mt2.recycle();
+ }
+
+ @Test
+ public void testFor70146381() {
+ MeasuredParagraph.buildForMeasurement(paint, "X…", 0, 2, RTL, null);
+ }
+
+ @ForType(MeasuredParagraph.class)
+ interface MeasuredParagraphReflector {
+ long getNativePtr();
+
+ @Static
+ MeasuredParagraph buildForStaticLayout(
+ TextPaint paint,
+ CharSequence text,
+ int start,
+ int end,
+ TextDirectionHeuristic textDir,
+ boolean computeHyphenation,
+ boolean computeLayout,
+ MeasuredParagraph recycle);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMeasuredTextTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMeasuredTextTest.java
new file mode 100644
index 000000000..e77896dd0
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeMeasuredTextTest.java
@@ -0,0 +1,123 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.Q;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.text.MeasuredText;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = Q)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeMeasuredTextTest {
+
+ private static Paint paint;
+
+ @Before
+ public void setup() {
+ paint = new Paint();
+ Context context = RuntimeEnvironment.getApplication();
+ AssetManager am = context.getAssets();
+ Typeface tf = new Typeface.Builder(am, "fonts/layout/linebreak.ttf").build();
+ paint.setTypeface(tf);
+ paint.setTextSize(10.0f); // Make 1em = 10px
+ }
+
+ @Test
+ public void testBuilder() {
+ String text = "Hello, World";
+ new MeasuredText.Builder(text.toCharArray())
+ .appendStyleRun(paint, text.length(), false /* isRtl */)
+ .build();
+ }
+
+ @Test
+ public void testBuilder_fromExistingMeasuredText() {
+ String text = "Hello, World";
+ final MeasuredText mt =
+ new MeasuredText.Builder(text.toCharArray())
+ .appendStyleRun(paint, text.length(), false /* isRtl */)
+ .build();
+ assertNotNull(
+ new MeasuredText.Builder(mt)
+ .appendStyleRun(paint, text.length(), true /* isRtl */)
+ .build());
+ }
+
+ @Test
+ public void testBuilder_fromExistingMeasuredText_differentLayoutParam() {
+ String text = "Hello, World";
+ final MeasuredText mt =
+ new MeasuredText.Builder(text.toCharArray())
+ .setComputeLayout(false)
+ .appendStyleRun(paint, text.length(), false /* isRtl */)
+ .build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new MeasuredText.Builder(mt)
+ .appendStyleRun(paint, text.length(), true /* isRtl */)
+ .build());
+ }
+
+ @Test
+ public void testBuilder_fromExistingMeasuredText_differentHyphenationParam() {
+ String text = "Hello, World";
+ final MeasuredText mt =
+ new MeasuredText.Builder(text.toCharArray())
+ .setComputeHyphenation(false)
+ .appendStyleRun(paint, text.length(), false /* isRtl */)
+ .build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new MeasuredText.Builder(mt)
+ .setComputeHyphenation(true)
+ .appendStyleRun(paint, text.length(), true /* isRtl */)
+ .build());
+ }
+
+ @Test
+ public void testBuilder_nullText() {
+ assertThrows(NullPointerException.class, () -> new MeasuredText.Builder((char[]) null));
+ }
+
+ @Test
+ public void testBuilder_nullMeasuredText() {
+ assertThrows(NullPointerException.class, () -> new MeasuredText.Builder((MeasuredText) null));
+ }
+
+ @Test
+ public void testBuilder_nullPaint() {
+ String text = "Hello, World";
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ new MeasuredText.Builder(text.toCharArray())
+ .appendStyleRun(null, text.length(), false));
+ }
+
+ @Test
+ public void testGetWidth() {
+ String text = "Hello, World";
+ MeasuredText mt =
+ new MeasuredText.Builder(text.toCharArray())
+ .appendStyleRun(paint, text.length(), false /* isRtl */)
+ .build();
+ assertEquals(0.0f, mt.getWidth(0, 0), 0.0f);
+ assertEquals(10.0f, mt.getWidth(0, 1), 0.0f);
+ assertEquals(20.0f, mt.getWidth(0, 2), 0.0f);
+ assertEquals(10.0f, mt.getWidth(1, 2), 0.0f);
+ assertEquals(20.0f, mt.getWidth(1, 3), 0.0f);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeNativeInterpolatorFactoryTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeNativeInterpolatorFactoryTest.java
new file mode 100644
index 000000000..d542cff6d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeNativeInterpolatorFactoryTest.java
@@ -0,0 +1,28 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.R;
+
+import android.graphics.animation.NativeInterpolatorFactory;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = R)
+public class ShadowNativeNativeInterpolatorFactoryTest {
+ @Test
+ public void testAllFunctionsLinkedProperly() {
+ NativeInterpolatorFactory.createAccelerateDecelerateInterpolator();
+ NativeInterpolatorFactory.createAccelerateInterpolator(0);
+ NativeInterpolatorFactory.createAnticipateInterpolator(0);
+ NativeInterpolatorFactory.createAnticipateOvershootInterpolator(0);
+ NativeInterpolatorFactory.createBounceInterpolator();
+ NativeInterpolatorFactory.createCycleInterpolator(0);
+ NativeInterpolatorFactory.createDecelerateInterpolator(0);
+ NativeInterpolatorFactory.createLinearInterpolator();
+ NativeInterpolatorFactory.createOvershootInterpolator(0);
+ NativeInterpolatorFactory.createPathInterpolator(new float[] {0}, new float[] {0});
+ NativeInterpolatorFactory.createLutInterpolator(new float[] {0});
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePaintTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePaintTest.java
new file mode 100644
index 000000000..924da1a2c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePaintTest.java
@@ -0,0 +1,2202 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.graphics.Paint.CURSOR_AFTER;
+import static android.graphics.Paint.CURSOR_AT;
+import static android.graphics.Paint.CURSOR_AT_OR_AFTER;
+import static android.graphics.Paint.CURSOR_AT_OR_BEFORE;
+import static android.graphics.Paint.CURSOR_BEFORE;
+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.TIRAMISU;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.MaskFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Cap;
+import android.graphics.Paint.Join;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.PathEffect;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.Typeface;
+import android.graphics.Xfermode;
+import android.os.Build;
+import android.os.LocaleList;
+import android.text.SpannedString;
+import java.util.Arrays;
+import java.util.Locale;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.reflector.ForType;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowNativePaintTest {
+
+ // These are hidden static fields in Paint.
+ private static final int DIRECTION_LTR = 0;
+ private static final int DIRECTION_RTL = 1;
+
+ private static final Typeface[] TYPEFACES =
+ new Typeface[] {
+ Typeface.DEFAULT,
+ Typeface.DEFAULT_BOLD,
+ Typeface.MONOSPACE,
+ Typeface.SANS_SERIF,
+ Typeface.SERIF,
+ };
+
+ @Test
+ public void testCtor() {
+ assertThat(new Paint(Paint.ANTI_ALIAS_FLAG).isAntiAlias()).isTrue();
+ assertThat(new Paint(0).isAntiAlias()).isFalse();
+ }
+
+ @Test
+ public void testCtorWithPaint() {
+ Paint paint = new Paint();
+ paint.setColor(Color.RED);
+ paint.setFlags(2345);
+
+ Paint other = new Paint(paint);
+ assertThat(other.getColor()).isEqualTo(Color.RED);
+ assertThat(other.getFlags()).isEqualTo(2345);
+ }
+
+ @Test
+ public void shouldGetAndSetTextAlignment() {
+ Paint paint = new Paint();
+ assertThat(paint.getTextAlign()).isEqualTo(Paint.Align.LEFT);
+ paint.setTextAlign(Paint.Align.CENTER);
+ assertThat(paint.getTextAlign()).isEqualTo(Paint.Align.CENTER);
+ }
+
+ @Test
+ public void shouldSetUnderlineText() {
+ Paint paint = new Paint();
+ paint.setUnderlineText(true);
+ assertThat(paint.isUnderlineText()).isTrue();
+ paint.setUnderlineText(false);
+ assertThat(paint.isUnderlineText()).isFalse();
+ }
+
+ @Test
+ public void measureTextActuallyMeasuresLength() {
+ Paint paint = new Paint();
+ paint.setTypeface(Typeface.DEFAULT);
+ assertThat(paint.measureText("Hello")).isEqualTo(28.0f);
+ assertThat(paint.measureText("Hello", 1, 3)).isEqualTo(9.0f);
+ assertThat(paint.measureText(new StringBuilder("Hello"), 1, 4)).isEqualTo(12.0f);
+ }
+
+ @Test
+ public void createPaintFromPaint() {
+ Paint origPaint = new Paint();
+ assertThat(new Paint(origPaint).getTextLocale()).isSameInstanceAs(origPaint.getTextLocale());
+ }
+
+ @Test
+ public void breakTextReturnsNonZeroResult() {
+ Paint paint = new Paint();
+ paint.setTypeface(Typeface.DEFAULT);
+ assertThat(
+ paint.breakText(
+ new char[] {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'},
+ /*index=*/ 0,
+ /*count=*/ 11,
+ /*maxWidth=*/ 100,
+ /*measuredWidth=*/ null))
+ .isGreaterThan(0);
+ assertThat(
+ paint.breakText(
+ "Hello World",
+ /*start=*/ 0,
+ /*end=*/ 11,
+ /*measureForwards=*/ true,
+ /*maxWidth=*/ 100,
+ /*measuredWidth=*/ null))
+ .isGreaterThan(0);
+ assertThat(
+ paint.breakText(
+ "Hello World",
+ /*measureForwards=*/ true,
+ /*maxWidth=*/ 100,
+ /*measuredWidth=*/ null))
+ .isGreaterThan(0);
+ }
+
+ @Test
+ public void test_setTypeface_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setTypeface(Typeface.DEFAULT);
+ assertThat(paint.getTypeface()).isEqualTo(Typeface.DEFAULT);
+ }
+
+ @Test
+ public void test_reset_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setColor(Color.BLACK);
+ paint.reset();
+ assertThat(paint.getColor()).isEqualTo(Color.BLACK);
+ }
+
+ @Test
+ public void test_getColor_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setColor(Color.BLACK);
+ assertThat(paint.getColor()).isEqualTo(Color.BLACK);
+ paint.setColor(Color.CYAN);
+ assertThat(paint.getColor()).isEqualTo(Color.CYAN);
+ }
+
+ @Test
+ public void test_getAlpha_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setAlpha(50);
+ assertThat(paint.getAlpha()).isEqualTo(50);
+
+ paint.setARGB(25, 0, 0, 0);
+ assertThat(paint.getAlpha()).isEqualTo(25);
+ assertThat(Integer.toHexString(paint.getColor())).isEqualTo("19000000");
+ }
+
+ @Test
+ public void test_getFlags_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setFlags(44);
+ assertThat(paint.getFlags()).isEqualTo(44);
+ }
+
+ @Test
+ public void test_isStrikeThruText_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setStrikeThruText(true);
+ assertThat(paint.isStrikeThruText()).isTrue();
+ }
+
+ @Test
+ public void test_isUnderlineText_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setUnderlineText(true);
+ assertThat(paint.isUnderlineText()).isTrue();
+ }
+
+ @Test
+ public void test_isDither_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setDither(true);
+ assertThat(paint.isDither()).isTrue();
+ }
+
+ @Test
+ public void test_isLinearText_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setLinearText(true);
+ assertThat(paint.isLinearText()).isTrue();
+ }
+
+ @Test
+ public void test_isAntiAlias_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setAntiAlias(true);
+ assertThat(paint.isAntiAlias()).isTrue();
+ }
+
+ @Test
+ public void test_isSubpixelText_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setSubpixelText(true);
+ assertThat(paint.isSubpixelText()).isTrue();
+ }
+
+ @Test
+ public void test_isFakeBoldText_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setFakeBoldText(true);
+ assertThat(paint.isFakeBoldText()).isTrue();
+ }
+
+ @Test
+ public void test_isFilterBitmap_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setFilterBitmap(true);
+ assertThat(paint.isFilterBitmap()).isTrue();
+ }
+
+ @Test
+ public void test_isElegantTextHeight_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setElegantTextHeight(true);
+ assertThat(paint.isElegantTextHeight()).isTrue();
+ }
+
+ @Test
+ public void test_getColorFilter_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ ColorFilter colorFilter = new ColorFilter();
+ paint.setColorFilter(colorFilter);
+ assertThat(paint.getColorFilter()).isEqualTo(colorFilter);
+ }
+
+ @Test
+ public void test_getStrokeWidth_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setStrokeWidth(15.0f);
+ assertThat(paint.getStrokeWidth()).isEqualTo(15.0f);
+ }
+
+ @Test
+ public void test_getStrokeMiter_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setStrokeMiter(15.0f);
+ assertThat(paint.getStrokeMiter()).isEqualTo(15.0f);
+ }
+
+ @Test
+ public void test_getStrokeCap_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setStrokeCap(Cap.BUTT);
+ assertThat(paint.getStrokeCap()).isEqualTo(Cap.BUTT);
+ }
+
+ @Test
+ public void test_getStrokeJoin_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setStrokeJoin(Paint.Join.ROUND);
+ assertThat(paint.getStrokeJoin()).isEqualTo(Paint.Join.ROUND);
+ }
+
+ @Test
+ public void test_getHinting_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setHinting(Paint.HINTING_ON);
+ assertThat(paint.getHinting()).isEqualTo(Paint.HINTING_ON);
+ }
+
+ @Test
+ public void test_getShader_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ Shader shader = new Shader();
+ paint.setShader(shader);
+ assertThat(paint.getShader()).isEqualTo(shader);
+ }
+
+ @Test
+ public void test_getXfermode_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ PorterDuffXfermode mode = new PorterDuffXfermode(Mode.SRC);
+ paint.setXfermode(mode);
+ assertThat(paint.getXfermode()).isEqualTo(mode);
+ }
+
+ @Test
+ public void test_getTextSize_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setTextSize(3.0f);
+ assertThat(paint.getTextSize()).isEqualTo(3.0f);
+ }
+
+ @Test
+ public void test_getTextLocale_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setTextLocale(Locale.US);
+ assertThat(paint.getTextLocale()).isEqualTo(Locale.US);
+ }
+
+ @Test
+ public void test_getTextScaleX_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setTextScaleX(5.0f);
+ assertThat(paint.getTextScaleX()).isEqualTo(5.0f);
+ }
+
+ @Test
+ public void test_getTextSkewX_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setTextSkewX(5.0f);
+ assertThat(paint.getTextSkewX()).isEqualTo(5.0f);
+ }
+
+ @Test
+ public void test_getTextAlign_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ paint.setTextAlign(Align.CENTER);
+ assertThat(paint.getTextAlign()).isEqualTo(Align.CENTER);
+ }
+
+ @Test
+ public void test_getShadowLayer_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ if (Build.VERSION.SDK_INT <= P) {
+ assertThat(reflector(PaintReflector.class, paint).hasShadowLayer()).isFalse();
+ paint.setShadowLayer(1.0f, 2.0f, 3.0f, 4);
+ assertThat(reflector(PaintReflector.class, paint).hasShadowLayer()).isTrue();
+ } else {
+ assertThat(reflector(PaintReflector.class, paint).hasShadowLayer()).isFalse();
+ paint.setShadowLayer(5.0f, 6.0f, 7.0f, 8L);
+ assertThat(paint.getShadowLayerRadius()).isEqualTo(5.0f);
+ assertThat(paint.getShadowLayerDx()).isEqualTo(6.0f);
+ assertThat(paint.getShadowLayerDy()).isEqualTo(7.0f);
+ assertThat(paint.getShadowLayerColorLong()).isEqualTo(8L);
+ assertThat(reflector(PaintReflector.class, paint).hasShadowLayer()).isTrue();
+ }
+ }
+
+ @Test
+ public void test_ascent_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ assertThat(paint.ascent()).isWithin(0.001f).of(-11.1328125f);
+ }
+
+ @Test
+ public void test_descent_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ assertThat(paint.descent()).isWithin(0.001f).of(2.9296875f);
+ }
+
+ @Test
+ public void test_getTextWidths_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ assertThat(paint.getTextWidths("12345", 0, 5, new float[] {5.0f, 5.0f, 5.0f, 5.0f, 5.0f}))
+ .isEqualTo(5);
+ }
+
+ @Test
+ public void test_getHasGlyph_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ assertTrue(paint.hasGlyph("A"));
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void test_getTextRunCursor_linkedProperly() {
+ Paint paint = new Paint();
+ assertThat(paint).isNotNull();
+ assertThat(paint.getTextRunCursor("SomeText", 0, 8, false, 0, CURSOR_AFTER)).isEqualTo(1);
+ }
+
+ // Begin O only tests
+ @Test
+ public void testBreakText() {
+ String text = "HIJKLMN";
+ char[] textChars = text.toCharArray();
+ SpannedString textSpan = new SpannedString(text);
+
+ Paint p = new Paint();
+
+ // We need to turn off kerning in order to get accurate comparisons
+ p.setFlags(p.getFlags() & ~Paint.DEV_KERN_TEXT_FLAG);
+
+ float[] widths = new float[text.length()];
+ assertEquals(text.length(), p.getTextWidths(text, widths));
+
+ float totalWidth = 0.0f;
+ for (int i = 0; i < text.length(); i++) {
+ totalWidth += widths[i];
+ }
+
+ for (int i = 0; i < text.length(); i++) {
+ verifyBreakText(text, textChars, textSpan, i, i + 1, true, totalWidth, 1, widths[i]);
+ }
+
+ // Measure empty string
+ verifyBreakText(text, textChars, textSpan, 0, 0, true, totalWidth, 0, 0);
+
+ // Measure substring from front: "HIJ"
+ verifyBreakText(
+ text, textChars, textSpan, 0, 3, true, totalWidth, 3, widths[0] + widths[1] + widths[2]);
+
+ // Reverse measure substring from front: "HIJ"
+ verifyBreakText(
+ text, textChars, textSpan, 0, 3, false, totalWidth, 3, widths[0] + widths[1] + widths[2]);
+
+ // Measure substring from back: "MN"
+ verifyBreakText(text, textChars, textSpan, 5, 7, true, totalWidth, 2, widths[5] + widths[6]);
+
+ // Reverse measure substring from back: "MN"
+ verifyBreakText(text, textChars, textSpan, 5, 7, false, totalWidth, 2, widths[5] + widths[6]);
+
+ // Measure substring in the middle: "JKL"
+ verifyBreakText(
+ text, textChars, textSpan, 2, 5, true, totalWidth, 3, widths[2] + widths[3] + widths[4]);
+
+ // Reverse measure substring in the middle: "JKL"
+ verifyBreakText(
+ text, textChars, textSpan, 2, 5, false, totalWidth, 3, widths[2] + widths[3] + widths[4]);
+
+ // Measure substring in the middle and restrict width to the first 2 characters.
+ verifyBreakText(
+ text, textChars, textSpan, 2, 5, true, widths[2] + widths[3], 2, widths[2] + widths[3]);
+
+ // Reverse measure substring in the middle and restrict width to the last 2 characters.
+ verifyBreakText(
+ text, textChars, textSpan, 2, 5, false, widths[3] + widths[4], 2, widths[3] + widths[4]);
+
+ // a single Emoji (U+1f601)
+ String emoji = "\ud83d\ude01";
+ char[] emojiChars = emoji.toCharArray();
+ SpannedString emojiSpan = new SpannedString(emoji);
+
+ float[] emojiWidths = new float[emoji.length()];
+ assertEquals(emoji.length(), p.getTextWidths(emoji, emojiWidths));
+
+ // Measure substring with a cluster
+ verifyBreakText(emoji, emojiChars, emojiSpan, 0, 2, true, 0, 0, 0);
+
+ // Measure substring with a cluster
+ verifyBreakText(emoji, emojiChars, emojiSpan, 0, 2, true, emojiWidths[0], 2, emojiWidths[0]);
+
+ // Reverse measure substring with a cluster
+ verifyBreakText(emoji, emojiChars, emojiSpan, 0, 2, false, 0, 0, 0);
+
+ // Measure substring with a cluster
+ verifyBreakText(emoji, emojiChars, emojiSpan, 0, 2, false, emojiWidths[0], 2, emojiWidths[0]);
+ }
+
+ private void verifyBreakText(
+ String text,
+ char[] textChars,
+ SpannedString textSpan,
+ int start,
+ int end,
+ boolean measureForwards,
+ float maxWidth,
+ int expectedCount,
+ float expectedWidth) {
+ Paint p = new Paint();
+
+ // We need to turn off kerning in order to get accurate comparisons
+ p.setFlags(p.getFlags() & ~Paint.DEV_KERN_TEXT_FLAG);
+
+ int count = end - start;
+ if (!measureForwards) {
+ count = -count;
+ }
+
+ float[][] measured = new float[][] {new float[1], new float[1], new float[1]};
+ String textSlice = text.substring(start, end);
+ assertEquals(expectedCount, p.breakText(textSlice, measureForwards, maxWidth, measured[0]));
+ assertEquals(expectedCount, p.breakText(textChars, start, count, maxWidth, measured[1]));
+ assertEquals(
+ expectedCount, p.breakText(textSpan, start, end, measureForwards, maxWidth, measured[2]));
+
+ for (int i = 0; i < measured.length; i++) {
+ assertEquals("i: " + i, expectedWidth, measured[i][0], 0.0f);
+ }
+ }
+
+ @Test
+ public void testSet() {
+ Paint p = new Paint();
+ Paint p2 = new Paint();
+ ColorFilter c = new ColorFilter();
+ MaskFilter m = new MaskFilter();
+ PathEffect e = new PathEffect();
+ Shader s = new Shader();
+ Typeface t = Typeface.DEFAULT;
+ Xfermode x = new Xfermode();
+
+ p.setColorFilter(c);
+ p.setMaskFilter(m);
+ p.setPathEffect(e);
+ p.setShader(s);
+ p.setTypeface(t);
+ p.setXfermode(x);
+ p2.set(p);
+ assertEquals(c, p2.getColorFilter());
+ assertEquals(m, p2.getMaskFilter());
+ assertEquals(e, p2.getPathEffect());
+ assertEquals(s, p2.getShader());
+ assertEquals(t, p2.getTypeface());
+ assertEquals(x, p2.getXfermode());
+
+ p2.set(p2);
+ assertEquals(c, p2.getColorFilter());
+ assertEquals(m, p2.getMaskFilter());
+ assertEquals(e, p2.getPathEffect());
+ assertEquals(s, p2.getShader());
+ assertEquals(t, p2.getTypeface());
+ assertEquals(x, p2.getXfermode());
+
+ p.setColorFilter(null);
+ p.setMaskFilter(null);
+ p.setPathEffect(null);
+ p.setShader(null);
+ p.setTypeface(null);
+ p.setXfermode(null);
+ p2.set(p);
+ assertNull(p2.getColorFilter());
+ assertNull(p2.getMaskFilter());
+ assertNull(p2.getPathEffect());
+ assertNull(p2.getShader());
+ assertNull(p2.getTypeface());
+ assertNull(p2.getXfermode());
+
+ p2.set(p2);
+ assertNull(p2.getColorFilter());
+ assertNull(p2.getMaskFilter());
+ assertNull(p2.getPathEffect());
+ assertNull(p2.getShader());
+ assertNull(p2.getTypeface());
+ assertNull(p2.getXfermode());
+ }
+
+ @Test
+ public void testAccessStrokeCap() {
+ Paint p = new Paint();
+
+ p.setStrokeCap(Cap.BUTT);
+ assertEquals(Cap.BUTT, p.getStrokeCap());
+
+ p.setStrokeCap(Cap.ROUND);
+ assertEquals(Cap.ROUND, p.getStrokeCap());
+
+ p.setStrokeCap(Cap.SQUARE);
+ assertEquals(Cap.SQUARE, p.getStrokeCap());
+ }
+
+ @Test
+ public void testSetStrokeCapNull() {
+ Paint p = new Paint();
+
+ assertThrows(RuntimeException.class, () -> p.setStrokeCap(null));
+ }
+
+ @Test
+ public void testAccessXfermode() {
+ Paint p = new Paint();
+ Xfermode x = new Xfermode();
+
+ assertEquals(x, p.setXfermode(x));
+ assertEquals(x, p.getXfermode());
+
+ assertNull(p.setXfermode(null));
+ assertNull(p.getXfermode());
+ }
+
+ @Test
+ public void testAccessShader() {
+ Paint p = new Paint();
+ Shader s = new Shader();
+
+ assertEquals(s, p.setShader(s));
+ assertEquals(s, p.getShader());
+
+ assertNull(p.setShader(null));
+ assertNull(p.getShader());
+ }
+
+ @Test
+ public void testShaderLocalMatrix() {
+ int width = 80;
+ int height = 120;
+ int[] color = new int[width * height];
+ Bitmap bitmap = Bitmap.createBitmap(color, width, height, Bitmap.Config.RGB_565);
+
+ Paint p = new Paint();
+ Matrix m = new Matrix();
+ Shader s = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+
+ // set the shaders matrix to a non identity value and attach to paint
+ m.setScale(10, 0);
+ s.setLocalMatrix(m);
+ p.setShader(s);
+
+ Matrix m2 = new Matrix();
+ assertTrue(p.getShader().getLocalMatrix(m2));
+ assertEquals(m, m2);
+
+ // updated the matrix again and set it on the shader but NOT the paint
+ m.setScale(0, 10);
+ s.setLocalMatrix(m);
+
+ // assert that the matrix on the paint's shader also changed
+ Matrix m3 = new Matrix();
+ assertTrue(p.getShader().getLocalMatrix(m3));
+ assertEquals(m, m3);
+ }
+
+ @Test
+ public void testSetAntiAlias() {
+ Paint p = new Paint();
+
+ p.setAntiAlias(true);
+ assertTrue(p.isAntiAlias());
+
+ p.setAntiAlias(false);
+ assertFalse(p.isAntiAlias());
+ }
+
+ @Test
+ public void testAccessTypeface() {
+ Paint p = new Paint();
+
+ assertEquals(Typeface.DEFAULT, p.setTypeface(Typeface.DEFAULT));
+ assertEquals(Typeface.DEFAULT, p.getTypeface());
+
+ assertEquals(Typeface.DEFAULT_BOLD, p.setTypeface(Typeface.DEFAULT_BOLD));
+ assertEquals(Typeface.DEFAULT_BOLD, p.getTypeface());
+
+ assertEquals(Typeface.MONOSPACE, p.setTypeface(Typeface.MONOSPACE));
+ assertEquals(Typeface.MONOSPACE, p.getTypeface());
+
+ assertNull(p.setTypeface(null));
+ assertNull(p.getTypeface());
+ }
+
+ @Test
+ public void testAccessPathEffect() {
+ Paint p = new Paint();
+ PathEffect e = new PathEffect();
+
+ assertEquals(e, p.setPathEffect(e));
+ assertEquals(e, p.getPathEffect());
+
+ assertNull(p.setPathEffect(null));
+ assertNull(p.getPathEffect());
+ }
+
+ @Test
+ public void testSetFakeBoldText() {
+ Paint p = new Paint();
+
+ p.setFakeBoldText(true);
+ assertTrue(p.isFakeBoldText());
+
+ p.setFakeBoldText(false);
+ assertFalse(p.isFakeBoldText());
+ }
+
+ @Test
+ public void testAccessStrokeJoin() {
+ Paint p = new Paint();
+
+ p.setStrokeJoin(Join.BEVEL);
+ assertEquals(Join.BEVEL, p.getStrokeJoin());
+
+ p.setStrokeJoin(Join.MITER);
+ assertEquals(Join.MITER, p.getStrokeJoin());
+
+ p.setStrokeJoin(Join.ROUND);
+ assertEquals(Join.ROUND, p.getStrokeJoin());
+ }
+
+ @Test
+ public void testSetStrokeJoinNull() {
+ Paint p = new Paint();
+
+ assertThrows(RuntimeException.class, () -> p.setStrokeJoin(null));
+ }
+
+ @Test
+ public void testAccessStyle() {
+ Paint p = new Paint();
+
+ p.setStyle(Style.FILL);
+ assertEquals(Style.FILL, p.getStyle());
+
+ p.setStyle(Style.FILL_AND_STROKE);
+ assertEquals(Style.FILL_AND_STROKE, p.getStyle());
+
+ p.setStyle(Style.STROKE);
+ assertEquals(Style.STROKE, p.getStyle());
+ }
+
+ @Test
+ public void testSetStyleNull() {
+ Paint p = new Paint();
+
+ assertThrows(RuntimeException.class, () -> p.setStyle(null));
+ }
+
+ @Test
+ public void testGetFontSpacing() {
+ Paint p = new Paint();
+
+ for (Typeface typeface : TYPEFACES) {
+ p.setTypeface(typeface);
+
+ p.setTextSize(10);
+ float spacing10 = p.getFontSpacing();
+ assertThat(spacing10).isGreaterThan(0);
+
+ p.setTextSize(20);
+ float spacing20 = p.getFontSpacing();
+ assertThat(spacing20).isGreaterThan(spacing10);
+ }
+ }
+
+ @Test
+ public void testSetSubpixelText() {
+ Paint p = new Paint();
+
+ p.setSubpixelText(true);
+ assertTrue(p.isSubpixelText());
+
+ p.setSubpixelText(false);
+ assertFalse(p.isSubpixelText());
+ }
+
+ @Test
+ public void testAccessTextScaleX() {
+ Paint p = new Paint();
+
+ p.setTextScaleX(2.0f);
+ assertEquals(2.0f, p.getTextScaleX(), 0.0f);
+
+ p.setTextScaleX(1.0f);
+ assertEquals(1.0f, p.getTextScaleX(), 0.0f);
+
+ p.setTextScaleX(0.0f);
+ assertEquals(0.0f, p.getTextScaleX(), 0.0f);
+ }
+
+ @Test
+ public void testAccessMaskFilter() {
+ Paint p = new Paint();
+ MaskFilter m = new MaskFilter();
+
+ assertEquals(m, p.setMaskFilter(m));
+ assertEquals(m, p.getMaskFilter());
+
+ assertNull(p.setMaskFilter(null));
+ assertNull(p.getMaskFilter());
+ }
+
+ @Test
+ public void testAccessColorFilter() {
+ Paint p = new Paint();
+ ColorFilter c = new ColorFilter();
+
+ assertEquals(c, p.setColorFilter(c));
+ assertEquals(c, p.getColorFilter());
+
+ assertNull(p.setColorFilter(null));
+ assertNull(p.getColorFilter());
+ }
+
+ @Test
+ public void testSetARGB() {
+ Paint p = new Paint();
+
+ p.setARGB(0, 0, 0, 0);
+ assertEquals(0, p.getColor());
+
+ p.setARGB(3, 3, 3, 3);
+ assertEquals((3 << 24) | (3 << 16) | (3 << 8) | 3, p.getColor());
+ }
+
+ @Test
+ public void testAscent() {
+ Paint p = new Paint();
+
+ for (Typeface typeface : TYPEFACES) {
+ p.setTypeface(typeface);
+
+ p.setTextSize(10);
+ float ascent10 = p.ascent();
+ assertThat(ascent10).isLessThan(0);
+
+ p.setTextSize(20);
+ float ascent20 = p.ascent();
+ assertThat(ascent20).isLessThan(ascent10);
+ }
+ }
+
+ @Test
+ public void testAccessTextSkewX() {
+ Paint p = new Paint();
+
+ p.setTextSkewX(1.0f);
+ assertEquals(1.0f, p.getTextSkewX(), 0.0f);
+
+ p.setTextSkewX(0.0f);
+ assertEquals(0.0f, p.getTextSkewX(), 0.0f);
+
+ p.setTextSkewX(-0.25f);
+ assertEquals(-0.25f, p.getTextSkewX(), 0.0f);
+ }
+
+ @Test
+ public void testAccessTextSize() {
+ Paint p = new Paint();
+
+ p.setTextSize(1.0f);
+ assertEquals(1.0f, p.getTextSize(), 0.0f);
+
+ p.setTextSize(2.0f);
+ assertEquals(2.0f, p.getTextSize(), 0.0f);
+
+ // text size should be greater than 0, so set -1 has no effect
+ p.setTextSize(-1.0f);
+ assertEquals(2.0f, p.getTextSize(), 0.0f);
+
+ // text size should be greater than or equals to 0
+ p.setTextSize(0.0f);
+ assertEquals(0.0f, p.getTextSize(), 0.0f);
+ }
+
+ @Test
+ public void testGetTextWidths() throws Exception {
+ String text = "HIJKLMN";
+ char[] textChars = text.toCharArray();
+ SpannedString textSpan = new SpannedString(text);
+
+ // Test measuring the widths of the entire text
+ verifyGetTextWidths(text, textChars, textSpan, 0, 7);
+
+ // Test measuring a substring of the text
+ verifyGetTextWidths(text, textChars, textSpan, 1, 3);
+
+ // Test measuring a substring of zero length.
+ verifyGetTextWidths(text, textChars, textSpan, 3, 3);
+
+ // Test measuring substrings from the front and back
+ verifyGetTextWidths(text, textChars, textSpan, 0, 2);
+ verifyGetTextWidths(text, textChars, textSpan, 4, 7);
+ }
+
+ /** Tests all four overloads of getTextWidths are the same. */
+ private void verifyGetTextWidths(
+ String text, char[] textChars, SpannedString textSpan, int start, int end) {
+ Paint p = new Paint();
+ int count = end - start;
+ float[][] widths =
+ new float[][] {new float[count], new float[count], new float[count], new float[count]};
+
+ String textSlice = text.substring(start, end);
+ assertEquals(count, p.getTextWidths(textSlice, widths[0]));
+ assertEquals(count, p.getTextWidths(textChars, start, count, widths[1]));
+ assertEquals(count, p.getTextWidths(textSpan, start, end, widths[2]));
+ assertEquals(count, p.getTextWidths(text, start, end, widths[3]));
+
+ // Check that the widths returned by the overloads are the same.
+ for (int i = 0; i < count; i++) {
+ assertEquals(widths[0][i], widths[1][i], 0.0f);
+ assertEquals(widths[1][i], widths[2][i], 0.0f);
+ assertEquals(widths[2][i], widths[3][i], 0.0f);
+ }
+ }
+
+ @Test
+ public void testSetStrikeThruText() {
+ Paint p = new Paint();
+
+ p.setStrikeThruText(true);
+ assertTrue(p.isStrikeThruText());
+
+ p.setStrikeThruText(false);
+ assertFalse(p.isStrikeThruText());
+ }
+
+ @Test
+ public void testAccessTextAlign() {
+ Paint p = new Paint();
+
+ p.setTextAlign(Align.CENTER);
+ assertEquals(Align.CENTER, p.getTextAlign());
+
+ p.setTextAlign(Align.LEFT);
+ assertEquals(Align.LEFT, p.getTextAlign());
+
+ p.setTextAlign(Align.RIGHT);
+ assertEquals(Align.RIGHT, p.getTextAlign());
+ }
+
+ @Test
+ public void testAccessTextLocale() {
+ Paint p = new Paint();
+
+ final Locale defaultLocale = Locale.getDefault();
+
+ // Check default
+ assertEquals(defaultLocale, p.getTextLocale());
+
+ // Check setter / getters
+ p.setTextLocale(Locale.US);
+ assertEquals(Locale.US, p.getTextLocale());
+ assertEquals(new LocaleList(Locale.US), p.getTextLocales());
+
+ p.setTextLocale(Locale.CHINESE);
+ assertEquals(Locale.CHINESE, p.getTextLocale());
+ assertEquals(new LocaleList(Locale.CHINESE), p.getTextLocales());
+
+ p.setTextLocale(Locale.JAPANESE);
+ assertEquals(Locale.JAPANESE, p.getTextLocale());
+ assertEquals(new LocaleList(Locale.JAPANESE), p.getTextLocales());
+
+ p.setTextLocale(Locale.KOREAN);
+ assertEquals(Locale.KOREAN, p.getTextLocale());
+ assertEquals(new LocaleList(Locale.KOREAN), p.getTextLocales());
+
+ // Check reverting back to default
+ p.setTextLocale(defaultLocale);
+ assertEquals(defaultLocale, p.getTextLocale());
+ assertEquals(new LocaleList(defaultLocale), p.getTextLocales());
+ }
+
+ @Test
+ public void testSetTextLocaleNull() {
+ Paint p = new Paint();
+
+ assertThrows(IllegalArgumentException.class, () -> p.setTextLocale(null));
+ }
+
+ @Test
+ public void testAccessTextLocales() {
+ Paint p = new Paint();
+
+ final LocaleList defaultLocales = LocaleList.getDefault();
+
+ // Check default
+ assertEquals(defaultLocales, p.getTextLocales());
+
+ // Check setter / getters for a one-member locale list
+ p.setTextLocales(new LocaleList(Locale.CHINESE));
+ assertEquals(Locale.CHINESE, p.getTextLocale());
+ assertEquals(new LocaleList(Locale.CHINESE), p.getTextLocales());
+
+ // Check setter / getters for a two-member locale list
+ p.setTextLocales(LocaleList.forLanguageTags("fr,de"));
+ assertEquals(Locale.forLanguageTag("fr"), p.getTextLocale());
+ assertEquals(LocaleList.forLanguageTags("fr,de"), p.getTextLocales());
+
+ // Check reverting back to default
+ p.setTextLocales(defaultLocales);
+ assertEquals(defaultLocales, p.getTextLocales());
+ }
+
+ @Test
+ public void testAccessTextLocalesNull() {
+ Paint p = new Paint();
+
+ // Check that we cannot pass a null locale list
+ assertThrows(IllegalArgumentException.class, () -> p.setTextLocales(null));
+ }
+
+ @Test
+ public void testAccessTextLocalesEmpty() {
+ Paint p = new Paint();
+
+ // Check that we cannot pass an empty locale list
+ assertThrows(IllegalArgumentException.class, () -> p.setTextLocales(new LocaleList()));
+ }
+
+ @Test
+ public void testGetFillPath() {
+ Paint p = new Paint();
+ Path path1 = new Path();
+ Path path2 = new Path();
+
+ assertTrue(path1.isEmpty());
+ assertTrue(path2.isEmpty());
+ p.getFillPath(path1, path2);
+ assertTrue(path1.isEmpty());
+ assertTrue(path2.isEmpty());
+
+ // No setter
+ }
+
+ @Test
+ public void testAccessAlpha() {
+ Paint p = new Paint();
+
+ p.setAlpha(0);
+ assertEquals(0, p.getAlpha());
+
+ p.setAlpha(255);
+ assertEquals(255, p.getAlpha());
+ }
+
+ @Test
+ public void testSetFilterBitmap() {
+ Paint p = new Paint();
+
+ p.setFilterBitmap(true);
+ assertTrue(p.isFilterBitmap());
+
+ p.setFilterBitmap(false);
+ assertFalse(p.isFilterBitmap());
+ }
+
+ @Test
+ public void testAccessColor() {
+ Paint p = new Paint();
+
+ p.setColor(1);
+ assertEquals(1, p.getColor());
+
+ p.setColor(0);
+ assertEquals(0, p.getColor());
+
+ p.setColor(255);
+ assertEquals(255, p.getColor());
+
+ p.setColor(-1);
+ assertEquals(-1, p.getColor());
+
+ p.setColor(256);
+ assertEquals(256, p.getColor());
+ }
+
+ @Test
+ public void testSetShadowLayer() {
+ new Paint().setShadowLayer(10, 1, 1, 0);
+ }
+
+ @Test
+ public void testGetFontMetrics1() {
+ Paint p = new Paint();
+ Paint.FontMetrics fm = new Paint.FontMetrics();
+
+ for (Typeface typeface : TYPEFACES) {
+ p.setTypeface(typeface);
+
+ p.setTextSize(10);
+ p.getFontMetrics(fm);
+ assertEquals(p.ascent(), fm.ascent, 0.0f);
+ assertEquals(p.descent(), fm.descent, 0.0f);
+
+ p.setTextSize(20);
+ p.getFontMetrics(fm);
+ assertEquals(p.ascent(), fm.ascent, 0.0f);
+ assertEquals(p.descent(), fm.descent, 0.0f);
+ }
+ }
+
+ @Test
+ public void testGetFontMetrics2() {
+ Paint p = new Paint();
+
+ for (Typeface typeface : TYPEFACES) {
+ p.setTypeface(typeface);
+
+ p.setTextSize(10);
+ Paint.FontMetrics fm = p.getFontMetrics();
+ assertEquals(p.ascent(), fm.ascent, 0.0f);
+ assertEquals(p.descent(), fm.descent, 0.0f);
+
+ p.setTextSize(20);
+ fm = p.getFontMetrics();
+ assertEquals(p.ascent(), fm.ascent, 0.0f);
+ assertEquals(p.descent(), fm.descent, 0.0f);
+ }
+ }
+
+ @Test
+ public void testAccessStrokeMiter() {
+ Paint p = new Paint();
+
+ p.setStrokeMiter(0.0f);
+ assertEquals(0.0f, p.getStrokeMiter(), 0.0f);
+
+ p.setStrokeMiter(10.0f);
+ assertEquals(10.0f, p.getStrokeMiter(), 0.0f);
+
+ // set value should be greater or equal to 0, set to -10.0f has no effect
+ p.setStrokeMiter(-10.0f);
+ assertEquals(10.0f, p.getStrokeMiter(), 0.0f);
+ }
+
+ @Test
+ public void testClearShadowLayer() {
+ new Paint().clearShadowLayer();
+ }
+
+ @Test
+ public void testSetUnderlineText() {
+ Paint p = new Paint();
+
+ p.setUnderlineText(true);
+ assertTrue(p.isUnderlineText());
+
+ p.setUnderlineText(false);
+ assertFalse(p.isUnderlineText());
+ }
+
+ @Test
+ public void testSetDither() {
+ Paint p = new Paint();
+
+ p.setDither(true);
+ assertTrue(p.isDither());
+
+ p.setDither(false);
+ assertFalse(p.isDither());
+ }
+
+ @Test
+ public void testDescent() {
+ Paint p = new Paint();
+
+ for (Typeface typeface : TYPEFACES) {
+ p.setTypeface(typeface);
+
+ p.setTextSize(10);
+ float descent10 = p.descent();
+ assertThat(descent10).isGreaterThan(0);
+
+ p.setTextSize(20);
+ float descent20 = p.descent();
+ assertThat(descent20).isGreaterThan(descent10);
+ }
+ }
+
+ @Test
+ public void testAccessFlags() {
+ Paint p = new Paint();
+
+ p.setFlags(Paint.ANTI_ALIAS_FLAG);
+ assertEquals(Paint.ANTI_ALIAS_FLAG, p.getFlags());
+
+ p.setFlags(Paint.DEV_KERN_TEXT_FLAG);
+ assertEquals(Paint.DEV_KERN_TEXT_FLAG, p.getFlags());
+ }
+
+ @Test
+ public void testAccessStrokeWidth() {
+ Paint p = new Paint();
+
+ p.setStrokeWidth(0.0f);
+ assertEquals(0.0f, p.getStrokeWidth(), 0.0f);
+
+ p.setStrokeWidth(10.0f);
+ assertEquals(10.0f, p.getStrokeWidth(), 0.0f);
+
+ // set value must greater or equal to 0, set -10.0f has no effect
+ p.setStrokeWidth(-10.0f);
+ assertEquals(10.0f, p.getStrokeWidth(), 0.0f);
+ }
+
+ @Test
+ public void testSetFontFeatureSettings() {
+ Paint p = new Paint();
+ // Roboto font (system default) has "fi" ligature
+ String text = "fi";
+ float[] widths = new float[text.length()];
+ p.getTextWidths(text, widths);
+ assertThat(widths[0]).isGreaterThan(0.0f);
+ assertEquals(0.0f, widths[1], 0.0f);
+
+ // Disable ligature using OpenType feature
+ p.setFontFeatureSettings("'liga' off");
+ p.getTextWidths(text, widths);
+ assertThat(widths[0]).isGreaterThan(0.0f);
+ assertThat(widths[1]).isGreaterThan(0.0f);
+
+ // Re-enable ligature
+ p.setFontFeatureSettings("'liga' on");
+ p.getTextWidths(text, widths);
+ assertThat(widths[0]).isGreaterThan(0.0f);
+ assertEquals(0.0f, widths[1], 0.0f);
+ }
+
+ @Test
+ public void testSetFontVariationSettings_defaultTypeface() {
+ new Paint().setFontVariationSettings("'wght' 400");
+ }
+
+ @Test
+ public void testGetTextBounds() {
+ Paint p = new Paint();
+ p.setTextSize(10);
+ String text1 = "hello";
+ Rect bounds1 = new Rect();
+ Rect bounds2 = new Rect();
+ p.getTextBounds(text1, 0, text1.length(), bounds1);
+ char[] textChars1 = text1.toCharArray();
+ p.getTextBounds(textChars1, 0, textChars1.length, bounds2);
+ // verify that string and char array methods produce consistent results
+ assertEquals(bounds1, bounds2);
+ String text2 = "hello world";
+
+ // verify substring produces consistent results
+ p.getTextBounds(text2, 0, text1.length(), bounds2);
+ assertEquals(bounds1, bounds2);
+
+ // longer string is expected to have same left edge but be wider
+ p.getTextBounds(text2, 0, text2.length(), bounds2);
+ assertEquals(bounds1.left, bounds2.left);
+ assertThat(bounds2.right).isGreaterThan(bounds1.right);
+
+ // bigger size implies bigger bounding rect
+ p.setTextSize(20);
+ p.getTextBounds(text1, 0, text1.length(), bounds2);
+ assertThat(bounds2.right).isGreaterThan(bounds1.right);
+ assertThat(bounds2.bottom - bounds2.top).isGreaterThan(bounds1.bottom - bounds1.top);
+ }
+
+ @Test
+ @Config(minSdk = O, maxSdk = O_MR1)
+ public void testReset() {
+ Paint p = new Paint();
+ ColorFilter c = new ColorFilter();
+ MaskFilter m = new MaskFilter();
+ PathEffect e = new PathEffect();
+ Shader s = new Shader();
+ Typeface t = Typeface.DEFAULT;
+ Xfermode x = new Xfermode();
+
+ p.setColorFilter(c);
+ p.setMaskFilter(m);
+ p.setPathEffect(e);
+ p.setShader(s);
+ p.setTypeface(t);
+ p.setXfermode(x);
+ p.setFlags(Paint.ANTI_ALIAS_FLAG);
+ assertEquals(c, p.getColorFilter());
+ assertEquals(m, p.getMaskFilter());
+ assertEquals(e, p.getPathEffect());
+ assertEquals(s, p.getShader());
+ assertEquals(t, p.getTypeface());
+ assertEquals(x, p.getXfermode());
+ assertEquals(Paint.ANTI_ALIAS_FLAG, p.getFlags());
+
+ p.reset();
+ assertEquals(Paint.DEV_KERN_TEXT_FLAG | Paint.EMBEDDED_BITMAP_TEXT_FLAG, p.getFlags());
+ assertNull(p.getColorFilter());
+ assertNull(p.getMaskFilter());
+ assertNull(p.getPathEffect());
+ assertNull(p.getShader());
+ assertNull(p.getTypeface());
+ assertNull(p.getXfermode());
+ }
+
+ @Test
+ public void testSetLinearText() {
+ Paint p = new Paint();
+
+ p.setLinearText(true);
+ assertTrue(p.isLinearText());
+
+ p.setLinearText(false);
+ assertFalse(p.isLinearText());
+ }
+
+ @Test
+ public void testGetFontMetricsInt1() {
+ Paint p = new Paint();
+ Paint.FontMetricsInt fmi = new Paint.FontMetricsInt();
+
+ for (Typeface typeface : TYPEFACES) {
+ p.setTypeface(typeface);
+
+ p.setTextSize(10);
+ p.getFontMetricsInt(fmi);
+ assertEquals(Math.round(p.ascent()), fmi.ascent);
+ assertEquals(Math.round(p.descent()), fmi.descent);
+
+ p.setTextSize(20);
+ p.getFontMetricsInt(fmi);
+ assertEquals(Math.round(p.ascent()), fmi.ascent);
+ assertEquals(Math.round(p.descent()), fmi.descent);
+ }
+ }
+
+ @Test
+ public void testGetFontMetricsInt2() {
+ Paint p = new Paint();
+ Paint.FontMetricsInt fmi;
+
+ for (Typeface typeface : TYPEFACES) {
+ p.setTypeface(typeface);
+
+ p.setTextSize(10);
+ fmi = p.getFontMetricsInt();
+ assertEquals(Math.round(p.ascent()), fmi.ascent);
+ assertEquals(Math.round(p.descent()), fmi.descent);
+
+ p.setTextSize(20);
+ fmi = p.getFontMetricsInt();
+ assertEquals(Math.round(p.ascent()), fmi.ascent);
+ assertEquals(Math.round(p.descent()), fmi.descent);
+ }
+ }
+
+ @Test
+ public void testMeasureText() {
+ String text = "HIJKLMN";
+ char[] textChars = text.toCharArray();
+ SpannedString textSpan = new SpannedString(text);
+
+ Paint p = new Paint();
+
+ // We need to turn off kerning in order to get accurate comparisons
+ p.setFlags(p.getFlags() & ~Paint.DEV_KERN_TEXT_FLAG);
+
+ float[] widths = new float[text.length()];
+ for (int i = 0; i < widths.length; i++) {
+ widths[i] = p.measureText(text, i, i + 1);
+ }
+
+ float totalWidth = 0;
+ for (int i = 0; i < widths.length; i++) {
+ totalWidth += widths[i];
+ }
+
+ // Test measuring the widths of the entire text
+ verifyMeasureText(text, textChars, textSpan, 0, 7, totalWidth);
+
+ // Test measuring a substring of the text
+ verifyMeasureText(text, textChars, textSpan, 1, 3, widths[1] + widths[2]);
+
+ // Test measuring a substring of zero length.
+ verifyMeasureText(text, textChars, textSpan, 3, 3, 0);
+
+ // Test measuring substrings from the front and back
+ verifyMeasureText(text, textChars, textSpan, 0, 2, widths[0] + widths[1]);
+ verifyMeasureText(text, textChars, textSpan, 4, 7, widths[4] + widths[5] + widths[6]);
+ }
+
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void getFontMetricsIntForText() {
+ Paint p = new Paint();
+ String str = "1234";
+ Paint.FontMetricsInt fmi = new Paint.FontMetricsInt();
+ p.getFontMetricsInt(str, 0, 4, 0, 4, false, fmi);
+ Paint.FontMetricsInt fmi2 = p.getFontMetricsInt();
+ assertThat(fmi).isEqualTo(fmi2);
+ }
+
+ @Test
+ public void testMeasureTextContext() {
+ Paint p = new Paint();
+ // Arabic LAM, which is different width depending on context
+ String shortString = "\u0644";
+ String longString = "\u0644\u0644\u0644";
+ char[] longChars = longString.toCharArray();
+ SpannedString longSpanned = new SpannedString(longString);
+ float width = p.measureText(shortString);
+ // Verify that measurement of substring is consistent no matter what surrounds it.
+ verifyMeasureText(longString, longChars, longSpanned, 0, 1, width);
+ verifyMeasureText(longString, longChars, longSpanned, 1, 2, width);
+ verifyMeasureText(longString, longChars, longSpanned, 2, 3, width);
+ }
+
+ @Test
+ public void testMeasureTextWithLongText() {
+ final int maxCount = 65535;
+ char[] longText = new char[maxCount];
+ Arrays.fill(longText, 0, maxCount, 'm');
+
+ Paint p = new Paint();
+ float width = p.measureText(longText, 0, 1);
+ assertThat(width).isGreaterThan(0);
+ }
+
+ /** Tests that all four overloads of measureText are the same and match some value. */
+ private void verifyMeasureText(
+ String text,
+ char[] textChars,
+ SpannedString textSpan,
+ int start,
+ int end,
+ float expectedWidth) {
+ Paint p = new Paint();
+
+ // We need to turn off kerning in order to get accurate comparisons
+ p.setFlags(p.getFlags() & ~Paint.DEV_KERN_TEXT_FLAG);
+
+ int count = end - start;
+ float[] widths = new float[] {-1, -1, -1, -1};
+
+ String textSlice = text.substring(start, end);
+ widths[0] = p.measureText(textSlice);
+ widths[1] = p.measureText(textChars, start, count);
+ widths[2] = p.measureText(textSpan, start, end);
+ widths[3] = p.measureText(text, start, end);
+
+ // Check that the widths returned by the overloads are the same.
+ assertEquals(widths[0], widths[1], 0.0f);
+ assertEquals(widths[1], widths[2], 0.0f);
+ assertEquals(widths[2], widths[3], 0.0f);
+ assertEquals(widths[3], expectedWidth, 0.0f);
+ }
+
+ @Test
+ public void testGetTextPathCharArray() {
+ Path path = new Path();
+
+ assertTrue(path.isEmpty());
+ new Paint().getTextPath(new char[] {'H', 'I', 'J', 'K', 'L', 'M', 'N'}, 0, 7, 0, 0, path);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testGetTextPathCharArrayNegativeIndex() {
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getTextPath(
+ new char[] {'H', 'I', 'J', 'K', 'L', 'M', 'N'}, -2, 7, 0, 0, new Path()));
+ }
+
+ @Test
+ public void testGetTextPathCharArrayNegativeCount() {
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getTextPath(
+ new char[] {'H', 'I', 'J', 'K', 'L', 'M', 'N'}, 0, -3, 0, 0, new Path()));
+ }
+
+ @Test
+ public void testGetTextPathCharArrayCountTooHigh() {
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getTextPath(
+ new char[] {'H', 'I', 'J', 'K', 'L', 'M', 'N'}, 3, 7, 0, 0, new Path()));
+ }
+
+ @Test
+ public void testGetTextPathString() {
+ Path path = new Path();
+
+ assertTrue(path.isEmpty());
+ new Paint().getTextPath("HIJKLMN", 0, 7, 0, 0, path);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testGetTextPathStringNegativeIndex() {
+ assertThrows(
+ RuntimeException.class, () -> new Paint().getTextPath("HIJKLMN", -2, 7, 0, 0, new Path()));
+ }
+
+ @Test
+ public void testGetTextPathStringNegativeCount() {
+ assertThrows(
+ RuntimeException.class, () -> new Paint().getTextPath("HIJKLMN", 0, -3, 0, 0, new Path()));
+ }
+
+ @Test
+ public void testGetTextPathStringStartTooHigh() {
+ assertThrows(
+ RuntimeException.class, () -> new Paint().getTextPath("HIJKLMN", 7, 3, 0, 0, new Path()));
+ }
+
+ @Test
+ public void testGetTextPathStringCountTooHigh() {
+ assertThrows(
+ RuntimeException.class, () -> new Paint().getTextPath("HIJKLMN", 3, 9, 0, 0, new Path()));
+ }
+
+ @Test
+ public void testHasGlyph() {
+ Paint p = new Paint();
+
+ // This method tests both the logic of hasGlyph and the sanity of fonts present
+ // on the device.
+ assertTrue(p.hasGlyph("A"));
+ assertFalse(p.hasGlyph("\uFFFE")); // U+FFFE is guaranteed to be a noncharacter
+
+ // Roboto 2 (the default typeface) does have an "fi" glyph and is mandated by CDD
+ assertTrue(p.hasGlyph("fi"));
+ assertFalse(p.hasGlyph("ab")); // but it does not contain an "ab" glyph
+ assertTrue(p.hasGlyph("\u02E5\u02E9")); // IPA tone mark ligature
+
+ // variation selectors
+ assertFalse(p.hasGlyph("a\uFE0F"));
+ assertFalse(p.hasGlyph("a\uDB40\uDDEF")); // UTF-16 encoding of U+E01EF
+ assertFalse(p.hasGlyph("\u2229\uFE0F")); // base character is in mathematical symbol font
+ // Note: U+FE0F is variation selection, unofficially reserved for emoji
+
+ // regional indicator symbols
+ assertTrue(p.hasGlyph("\uD83C\uDDEF\uD83C\uDDF5")); // "JP" U+1F1EF U+1F1F5
+ assertFalse(p.hasGlyph("\uD83C\uDDFF\uD83C\uDDFF")); // "ZZ" U+1F1FF U+1F1FF
+
+ // Mongolian, which is an optional font, but if present, should support FVS
+ if (p.hasGlyph("\u182D")) {
+ assertTrue(p.hasGlyph("\u182D\u180B"));
+ }
+
+ // Emoji with variation selector support for both text and emoji presentation
+ assertTrue(p.hasGlyph("\u231A\uFE0E")); // WATCH + VS15
+ assertTrue(p.hasGlyph("\u231A\uFE0F")); // WATCH + VS16
+
+ // Unicode 7.0, 8.0, and 9.0 emoji should be supported.
+ assertTrue(p.hasGlyph("\uD83D\uDD75")); // SLEUTH OR SPY is introduced in Unicode 7.0
+ assertTrue(p.hasGlyph("\uD83C\uDF2E")); // TACO is introduced in Unicode 8.0
+ assertTrue(p.hasGlyph("\uD83E\uDD33")); // SELFIE is introduced in Unicode 9.0
+
+ // We don't require gender-neutral emoji, but if present, results must be consistent
+ // whether VS is present or not.
+ assertTrue(
+ p.hasGlyph("\uD83D\uDC69\u200D\u2695")
+ == // WOMAN, ZWJ, STAFF OF AESCULAPIUS
+ p.hasGlyph("\uD83D\uDC69\u200D\u2695\uFE0F")); // above + VS16
+ }
+
+ @Test
+ public void testGetRunAdvance() {
+ Paint p = new Paint();
+ {
+ // LTR
+ String string = "abcdef";
+ {
+ final float width =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), false, 0);
+ assertEquals(0.0f, width, 0.0f);
+ }
+ {
+ for (int i = 0; i < string.length(); i++) {
+ final float width = p.getRunAdvance(string, i, i + 1, 0, string.length(), false, i);
+ assertEquals(0.0f, width, 0.0f);
+ }
+ }
+ {
+ final float widthToMid =
+ p.getRunAdvance(
+ string, 0, string.length(), 0, string.length(), false, string.length() / 2);
+ final float widthToTail =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), false, string.length());
+ assertThat(widthToMid).isGreaterThan(0.0f);
+ assertThat(widthToTail).isGreaterThan(widthToMid);
+ }
+ {
+ final float widthFromHead =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), false, string.length());
+ final float widthFromSecond =
+ p.getRunAdvance(string, 1, string.length(), 0, string.length(), false, string.length());
+ assertThat(widthFromHead).isGreaterThan(widthFromSecond);
+ }
+ {
+ float width = 0.0f;
+ for (int i = 0; i < string.length(); i++) {
+ width += p.getRunAdvance(string, i, i + 1, 0, string.length(), false, i + 1);
+ }
+ final float totalWidth =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), false, string.length());
+ assertEquals(totalWidth, width, 1.0f);
+ }
+ }
+ {
+ // RTL
+ String string = "\u0644\u063A\u0629 \u0639\u0631\u0628\u064A\u0629"; // Arabic
+ {
+ final float width =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), true, 0);
+ assertEquals(0.0f, width, 0.0f);
+ }
+ {
+ for (int i = 0; i < string.length(); i++) {
+ final float width = p.getRunAdvance(string, i, i + 1, 0, string.length(), true, i);
+ assertEquals(0.0f, width, 0.0f);
+ }
+ }
+ {
+ final float widthToMid =
+ p.getRunAdvance(
+ string, 0, string.length(), 0, string.length(), true, string.length() / 2);
+ final float widthToTail =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), true, string.length());
+ assertThat(widthToMid).isGreaterThan(0.0f);
+ assertThat(widthToTail).isGreaterThan(widthToMid);
+ }
+ {
+ final float widthFromHead =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), true, string.length());
+ final float widthFromSecond =
+ p.getRunAdvance(string, 1, string.length(), 0, string.length(), true, string.length());
+ assertThat(widthFromHead).isGreaterThan(widthFromSecond);
+ }
+ }
+ }
+
+ @Test
+ public void testGetRunAdvanceNullCharSequence() {
+ assertThrows(
+ RuntimeException.class,
+ () -> new Paint().getRunAdvance((CharSequence) null, 0, 0, 0, 0, false, 0));
+ }
+
+ @Test
+ public void testGetRunAdvanceNullCharArray() {
+ assertThrows(
+ RuntimeException.class,
+ () -> new Paint().getRunAdvance((char[]) null, 0, 0, 0, 0, false, 0));
+ }
+
+ @Test
+ public void testGetRunAdvanceTextLengthLessThenContextEnd() {
+ final String string = "abcde";
+
+ // text length < context end
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getRunAdvance(
+ string, 0, string.length(), 0, string.length() + 1, false, string.length()));
+ }
+
+ @Test
+ public void testGetRunAdvanceContextEndLessThanEnd() {
+ final String string = "abcde";
+
+ // context end < end
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getRunAdvance(string, 0, string.length(), 0, string.length() - 1, false, 0));
+ }
+
+ @Test
+ public void testGetRunAdvanceEndLessThanOffset() {
+ final String string = "abcde";
+
+ // end < offset
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getRunAdvance(
+ string,
+ 0,
+ string.length() - 1,
+ 0,
+ string.length() - 1,
+ false,
+ string.length()));
+ }
+
+ @Test
+ public void testGetRunAdvanceOffsetLessThanStart() {
+ final String string = "abcde";
+
+ // offset < start
+ assertThrows(
+ RuntimeException.class,
+ () -> new Paint().getRunAdvance(string, 1, string.length(), 1, string.length(), false, 0));
+ }
+
+ @Test
+ public void testGetRunAdvanceStartLessThanContextStart() {
+ final String string = "abcde";
+
+ // start < context start
+ assertThrows(
+ RuntimeException.class,
+ () -> new Paint().getRunAdvance(string, 0, string.length(), 1, string.length(), false, 1));
+ }
+
+ @Test
+ public void testGetRunAdvanceContextStartNegative() {
+ final String string = "abcde";
+
+ // context start < 0
+ assertThrows(
+ RuntimeException.class,
+ () -> new Paint().getRunAdvance(string, 0, string.length(), -1, string.length(), false, 0));
+ }
+
+ @Test
+ public void testGetRunAdvance_nonzeroIndex() {
+ Paint p = new Paint();
+ final String text =
+ "Android powers hundreds of millions of mobile "
+ + "devices in more than 190 countries around the world. It's"
+ + "the largest installed base of any mobile platform and"
+ + "growing fast—every day another million users power up their"
+ + "Android devices for the first time and start looking for"
+ + "apps, games, and other digital content.";
+ // Test offset index does not affect width.
+ final float widthAndroidFirst = p.getRunAdvance(text, 0, 7, 0, text.length(), false, 7);
+ final float widthAndroidSecond = p.getRunAdvance(text, 215, 222, 0, text.length(), false, 222);
+ assertThat(Math.abs(widthAndroidFirst - widthAndroidSecond)).isLessThan(1);
+ }
+
+ @Test
+ public void testGetRunAdvance_glyphDependingContext() {
+ Paint p = new Paint();
+ // Test the context change the character shape.
+ // First character should be isolated form because the context ends at index 1.
+ final float isolatedFormWidth = p.getRunAdvance("\u0644\u0644", 0, 1, 0, 1, true, 1);
+ // First character should be initial form because the context ends at index 2.
+ final float initialFormWidth = p.getRunAdvance("\u0644\u0644", 0, 1, 0, 2, true, 1);
+ assertThat(isolatedFormWidth).isGreaterThan(initialFormWidth);
+ }
+
+ @Test
+ @SuppressWarnings("UnicodeEscape")
+ public void testGetRunAdvance_arabic() {
+ Paint p = new Paint();
+ // Test total width is equals to sum of each character's width.
+ // "What is Unicode?" in Arabic.
+ final String text =
+ "\u0645\u0627\u0647\u064A\u0020\u0627\u0644\u0634"
+ + "\u0641\u0631\u0629\u0020\u0627\u0644\u0645\u0648\u062D"
+ + "\u062F\u0629\u0020\u064A\u0648\u0646\u064A\u0643\u0648"
+ + "\u062F\u061F";
+ final float totalWidth =
+ p.getRunAdvance(text, 0, text.length(), 0, text.length(), true, text.length());
+ float sumOfCharactersWidth = 0;
+ for (int i = 0; i < text.length(); i++) {
+ sumOfCharactersWidth += p.getRunAdvance(text, i, i + 1, 0, text.length(), true, i + 1);
+ }
+ assertThat(Math.abs(totalWidth - sumOfCharactersWidth)).isLessThan(1);
+ }
+
+ @Test
+ public void testGetOffsetForAdvance() {
+ Paint p = new Paint();
+ {
+ // LTR
+ String string = "abcdef";
+ {
+ for (int offset = 0; offset <= string.length(); ++offset) {
+ final float widthToOffset =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), false, offset);
+ final int restoredOffset =
+ p.getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length(), false, widthToOffset);
+ assertEquals(offset, restoredOffset);
+ }
+ }
+ {
+ final int offset =
+ p.getOffsetForAdvance(string, 0, string.length(), 0, string.length(), false, -10.0f);
+ assertEquals(0, offset);
+ }
+ {
+ final float widthToEnd =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), true, string.length());
+ final int offset =
+ p.getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length(), true, widthToEnd + 10.0f);
+ assertEquals(string.length(), offset);
+ }
+ }
+ {
+ // RTL
+ String string = "\u0639\u0631\u0628\u0649"; // Arabic
+ {
+ for (int offset = 0; offset <= string.length(); ++offset) {
+ final float widthToOffset =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), true, offset);
+ final int restoredOffset =
+ p.getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length(), true, widthToOffset);
+ assertEquals(offset, restoredOffset);
+ }
+ }
+ {
+ final int offset =
+ p.getOffsetForAdvance(string, 0, string.length(), 0, string.length(), true, -10.0f);
+ assertEquals(0, offset);
+ }
+ {
+ final float widthToEnd =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), true, string.length());
+ final int offset =
+ p.getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length(), true, widthToEnd + 10.0f);
+ assertEquals(string.length(), offset);
+ }
+ }
+ }
+
+ @Test
+ public void testGetOffsetForAdvanceNullCharSequence() {
+ assertThrows(
+ RuntimeException.class,
+ () -> new Paint().getOffsetForAdvance((CharSequence) null, 0, 0, 0, 0, false, 0.0f));
+ }
+
+ @Test
+ public void testGetOffsetForAdvanceNullCharArray() {
+ assertThrows(
+ RuntimeException.class,
+ () -> new Paint().getOffsetForAdvance((char[]) null, 0, 0, 0, 0, false, 0.0f));
+ }
+
+ @Test
+ public void testGetOffsetForAdvanceContextStartNegative() {
+ final String string = "abcde";
+
+ // context start < 0
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getOffsetForAdvance(string, -1, string.length(), 0, string.length(), false, 0.0f));
+ }
+
+ @Test
+ public void testGetOffsetForAdvanceStartLessThanContextStart() {
+ final String string = "abcde";
+
+ // start < context start
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getOffsetForAdvance(string, 0, string.length(), 1, string.length(), false, 0.0f));
+ }
+
+ @Test
+ public void testGetOffsetForAdvanceEndLessThanStart() {
+ final String string = "abcde";
+
+ // end < start
+ assertThrows(
+ RuntimeException.class,
+ () -> new Paint().getOffsetForAdvance(string, 1, 0, 0, 0, false, 0));
+ }
+
+ @Test
+ public void testGetOffsetForAdvanceContextEndLessThanEnd() {
+ final String string = "abcde";
+
+ // context end < end
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length() - 1, false, 0.0f));
+ }
+
+ @Test
+ public void testGetOffsetForAdvanceTextLengthLessThanContextEnd() {
+ final String string = "abcde";
+
+ // text length < context end
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ new Paint()
+ .getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length() + 1, false, 0.0f));
+ }
+
+ @Test
+ public void testGetOffsetForAdvance_graphemeCluster() {
+ Paint p = new Paint();
+ {
+ String string = "\uD83C\uDF37"; // U+1F337: TULIP
+ {
+ final float widthToOffset =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), false, 1);
+ final int offset =
+ p.getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length(), false, widthToOffset);
+ assertFalse(1 == offset);
+ assertTrue(0 == offset || string.length() == offset);
+ }
+ }
+ {
+ String string = "\uD83C\uDDFA\uD83C\uDDF8"; // US flag
+ {
+ final float widthToOffset =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), false, 2);
+ final int offset =
+ p.getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length(), false, widthToOffset);
+ assertFalse(2 == offset);
+ assertTrue(0 == offset || string.length() == offset);
+ }
+ {
+ final float widthToOffset = p.getRunAdvance(string, 0, 2, 0, 2, false, 2);
+ final int offset = p.getOffsetForAdvance(string, 0, 2, 0, 2, false, widthToOffset);
+ assertEquals(2, offset);
+ }
+ }
+ {
+ // HANGUL CHOSEONG KIYEOK, HANGUL JUNGSEONG A, HANDUL JONGSEONG KIYEOK
+ String string = "\u1100\u1161\u11A8";
+ {
+ for (int offset = 0; offset <= string.length(); ++offset) {
+ final float widthToOffset =
+ p.getRunAdvance(string, 0, string.length(), 0, string.length(), false, offset);
+ final int offsetForAdvance =
+ p.getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length(), false, widthToOffset);
+ assertTrue(0 == offsetForAdvance || string.length() == offsetForAdvance);
+ }
+ for (int offset = 0; offset <= string.length(); ++offset) {
+ final float widthToOffset = p.getRunAdvance(string, 0, offset, 0, offset, false, offset);
+ final int offsetForAdvance =
+ p.getOffsetForAdvance(
+ string, 0, string.length(), 0, string.length(), false, widthToOffset);
+ assertTrue(0 == offsetForAdvance || string.length() == offsetForAdvance);
+ }
+ for (int offset = 0; offset <= string.length(); ++offset) {
+ final float widthToOffset = p.getRunAdvance(string, 0, offset, 0, offset, false, offset);
+ final int offsetForAdvance =
+ p.getOffsetForAdvance(string, 0, offset, 0, offset, false, widthToOffset);
+ assertEquals(offset, offsetForAdvance);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testElegantText() {
+ final Paint p = new Paint();
+ p.setTextSize(10);
+ assertFalse(p.isElegantTextHeight());
+ final float nonElegantTop = p.getFontMetrics().top;
+ final float nonElegantBottom = p.getFontMetrics().bottom;
+
+ p.setElegantTextHeight(true);
+ assertTrue(p.isElegantTextHeight());
+ final float elegantTop = p.getFontMetrics().top;
+ final float elegantBottom = p.getFontMetrics().bottom;
+
+ assertThat(elegantTop).isLessThan(nonElegantTop);
+ assertThat(elegantBottom).isGreaterThan(nonElegantBottom);
+ p.setElegantTextHeight(false);
+ assertFalse(p.isElegantTextHeight());
+ }
+
+ private int getTextRunCursor(String text, int offset, int cursorOpt) {
+ final int contextStart = 0;
+ final int contextEnd = text.length();
+ final int contextCount = text.length();
+ Paint p = new Paint();
+ int result;
+ if (RuntimeEnvironment.getApiLevel() <= P) {
+ result =
+ reflector(PaintReflector.class, p)
+ .getTextRunCursor(
+ new StringBuilder(text), // as a CharSequence
+ contextStart,
+ contextEnd,
+ DIRECTION_LTR,
+ offset,
+ cursorOpt);
+ assertEquals(
+ result,
+ reflector(PaintReflector.class, p)
+ .getTextRunCursor(
+ text, contextStart, contextCount, DIRECTION_LTR, offset, cursorOpt));
+ assertEquals(
+ result,
+ reflector(PaintReflector.class, p)
+ .getTextRunCursor(
+ new StringBuilder(text), // as a CharSequence
+ contextStart,
+ contextCount,
+ DIRECTION_RTL,
+ offset,
+ cursorOpt));
+ assertEquals(
+ result,
+ reflector(PaintReflector.class, p)
+ .getTextRunCursor(
+ text, contextStart, contextCount, DIRECTION_RTL, offset, cursorOpt));
+ } else {
+ result =
+ p.getTextRunCursor(
+ new StringBuilder(text), // as a CharSequence
+ contextStart,
+ contextEnd,
+ false /* isRtl */,
+ offset,
+ cursorOpt);
+ assertEquals(
+ result,
+ p.getTextRunCursor(
+ text.toCharArray(),
+ contextStart,
+ contextCount,
+ false /* isRtl */,
+ offset,
+ cursorOpt));
+ assertEquals(
+ result,
+ p.getTextRunCursor(
+ new StringBuilder(text), // as a CharSequence
+ contextStart,
+ contextCount,
+ true /* isRtl */,
+ offset,
+ cursorOpt));
+ assertEquals(
+ result,
+ p.getTextRunCursor(
+ text.toCharArray(), contextStart, contextCount, true, offset, cursorOpt));
+ }
+ return result;
+ }
+
+ @Test
+ public void testGetRunCursor_cursor_after() {
+ assertEquals(1, getTextRunCursor("abc", 0, CURSOR_AFTER));
+ assertEquals(2, getTextRunCursor("abc", 1, CURSOR_AFTER));
+ assertEquals(3, getTextRunCursor("abc", 2, CURSOR_AFTER));
+ assertEquals(3, getTextRunCursor("abc", 3, CURSOR_AFTER));
+
+ // Surrogate pairs
+ assertEquals(1, getTextRunCursor("a\uD83D\uDE00c", 0, CURSOR_AFTER));
+ assertEquals(3, getTextRunCursor("a\uD83D\uDE00c", 1, CURSOR_AFTER));
+ assertEquals(3, getTextRunCursor("a\uD83D\uDE00c", 2, CURSOR_AFTER));
+ assertEquals(4, getTextRunCursor("a\uD83D\uDE00c", 3, CURSOR_AFTER));
+ assertEquals(4, getTextRunCursor("a\uD83D\uDE00c", 4, CURSOR_AFTER));
+
+ // Combining marks
+ assertEquals(1, getTextRunCursor("a\u0061\u0302c", 0, CURSOR_AFTER));
+ assertEquals(3, getTextRunCursor("a\u0061\u0302c", 1, CURSOR_AFTER));
+ assertEquals(3, getTextRunCursor("a\u0061\u0302c", 2, CURSOR_AFTER));
+ assertEquals(4, getTextRunCursor("a\u0061\u0302c", 3, CURSOR_AFTER));
+ assertEquals(4, getTextRunCursor("a\u0061\u0302c", 4, CURSOR_AFTER));
+ }
+
+ @Test
+ public void testGetRunCursor_currsor_at() {
+ assertEquals(0, getTextRunCursor("abc", 0, CURSOR_AT));
+ assertEquals(1, getTextRunCursor("abc", 1, CURSOR_AT));
+ assertEquals(2, getTextRunCursor("abc", 2, CURSOR_AT));
+ assertEquals(3, getTextRunCursor("abc", 3, CURSOR_AT));
+
+ // Surrogate pairs
+ assertEquals(0, getTextRunCursor("a\uD83D\uDE00c", 0, CURSOR_AT));
+ assertEquals(1, getTextRunCursor("a\uD83D\uDE00c", 1, CURSOR_AT));
+ assertEquals(-1, getTextRunCursor("a\uD83D\uDE00c", 2, CURSOR_AT));
+ assertEquals(3, getTextRunCursor("a\uD83D\uDE00c", 3, CURSOR_AT));
+ assertEquals(4, getTextRunCursor("a\uD83D\uDE00c", 4, CURSOR_AT));
+
+ // Combining marks
+ assertEquals(0, getTextRunCursor("a\u0061\u0302c", 0, CURSOR_AT));
+ assertEquals(1, getTextRunCursor("a\u0061\u0302c", 1, CURSOR_AT));
+ assertEquals(-1, getTextRunCursor("a\u0061\u0302c", 2, CURSOR_AT));
+ assertEquals(3, getTextRunCursor("a\u0061\u0302c", 3, CURSOR_AT));
+ assertEquals(4, getTextRunCursor("a\u0061\u0302c", 4, CURSOR_AT));
+ }
+
+ @Test
+ public void testGetRunCursor_cursor_at_or_after() {
+ assertEquals(0, getTextRunCursor("abc", 0, CURSOR_AT_OR_AFTER));
+ assertEquals(1, getTextRunCursor("abc", 1, CURSOR_AT_OR_AFTER));
+ assertEquals(2, getTextRunCursor("abc", 2, CURSOR_AT_OR_AFTER));
+ assertEquals(3, getTextRunCursor("abc", 3, CURSOR_AT_OR_AFTER));
+
+ // Surrogate pairs
+ assertEquals(0, getTextRunCursor("a\uD83D\uDE00c", 0, CURSOR_AT_OR_AFTER));
+ assertEquals(1, getTextRunCursor("a\uD83D\uDE00c", 1, CURSOR_AT_OR_AFTER));
+ assertEquals(3, getTextRunCursor("a\uD83D\uDE00c", 2, CURSOR_AT_OR_AFTER));
+ assertEquals(3, getTextRunCursor("a\uD83D\uDE00c", 3, CURSOR_AT_OR_AFTER));
+ assertEquals(4, getTextRunCursor("a\uD83D\uDE00c", 4, CURSOR_AT_OR_AFTER));
+
+ // Combining marks
+ assertEquals(0, getTextRunCursor("a\u0061\u0302c", 0, CURSOR_AT_OR_AFTER));
+ assertEquals(1, getTextRunCursor("a\u0061\u0302c", 1, CURSOR_AT_OR_AFTER));
+ assertEquals(3, getTextRunCursor("a\u0061\u0302c", 2, CURSOR_AT_OR_AFTER));
+ assertEquals(3, getTextRunCursor("a\u0061\u0302c", 3, CURSOR_AT_OR_AFTER));
+ assertEquals(4, getTextRunCursor("a\u0061\u0302c", 4, CURSOR_AT_OR_AFTER));
+ }
+
+ @Test
+ public void testGetRunCursor_cursor_at_or_before() {
+ assertEquals(0, getTextRunCursor("abc", 0, CURSOR_AT_OR_BEFORE));
+ assertEquals(1, getTextRunCursor("abc", 1, CURSOR_AT_OR_BEFORE));
+ assertEquals(2, getTextRunCursor("abc", 2, CURSOR_AT_OR_BEFORE));
+ assertEquals(3, getTextRunCursor("abc", 3, CURSOR_AT_OR_BEFORE));
+
+ // Surrogate pairs
+ assertEquals(0, getTextRunCursor("a\uD83D\uDE00c", 0, CURSOR_AT_OR_BEFORE));
+ assertEquals(1, getTextRunCursor("a\uD83D\uDE00c", 1, CURSOR_AT_OR_BEFORE));
+ assertEquals(1, getTextRunCursor("a\uD83D\uDE00c", 2, CURSOR_AT_OR_BEFORE));
+ assertEquals(3, getTextRunCursor("a\uD83D\uDE00c", 3, CURSOR_AT_OR_BEFORE));
+ assertEquals(4, getTextRunCursor("a\uD83D\uDE00c", 4, CURSOR_AT_OR_BEFORE));
+
+ // Combining marks
+ assertEquals(0, getTextRunCursor("a\u0061\u0302c", 0, CURSOR_AT_OR_BEFORE));
+ assertEquals(1, getTextRunCursor("a\u0061\u0302c", 1, CURSOR_AT_OR_BEFORE));
+ assertEquals(1, getTextRunCursor("a\u0061\u0302c", 2, CURSOR_AT_OR_BEFORE));
+ assertEquals(3, getTextRunCursor("a\u0061\u0302c", 3, CURSOR_AT_OR_BEFORE));
+ assertEquals(4, getTextRunCursor("a\u0061\u0302c", 4, CURSOR_AT_OR_BEFORE));
+ }
+
+ @Test
+ public void testGetRunCursor_cursor_before() {
+ assertEquals(0, getTextRunCursor("abc", 0, CURSOR_BEFORE));
+ assertEquals(0, getTextRunCursor("abc", 1, CURSOR_BEFORE));
+ assertEquals(1, getTextRunCursor("abc", 2, CURSOR_BEFORE));
+ assertEquals(2, getTextRunCursor("abc", 3, CURSOR_BEFORE));
+
+ // Surrogate pairs
+ assertEquals(0, getTextRunCursor("a\uD83D\uDE00c", 0, CURSOR_BEFORE));
+ assertEquals(0, getTextRunCursor("a\uD83D\uDE00c", 1, CURSOR_BEFORE));
+ assertEquals(1, getTextRunCursor("a\uD83D\uDE00c", 2, CURSOR_BEFORE));
+ assertEquals(1, getTextRunCursor("a\uD83D\uDE00c", 3, CURSOR_BEFORE));
+ assertEquals(3, getTextRunCursor("a\uD83D\uDE00c", 4, CURSOR_BEFORE));
+
+ // Combining marks
+ assertEquals(0, getTextRunCursor("a\u0061\u0302c", 0, CURSOR_BEFORE));
+ assertEquals(0, getTextRunCursor("a\u0061\u0302c", 1, CURSOR_BEFORE));
+ assertEquals(1, getTextRunCursor("a\u0061\u0302c", 2, CURSOR_BEFORE));
+ assertEquals(1, getTextRunCursor("a\u0061\u0302c", 3, CURSOR_BEFORE));
+ assertEquals(3, getTextRunCursor("a\u0061\u0302c", 4, CURSOR_BEFORE));
+ }
+
+ @ForType(Paint.class)
+ interface PaintReflector {
+ int getTextRunCursor(
+ CharSequence text, int contextStart, int contextEnd, int dir, int offset, int cursorOpt);
+
+ int getTextRunCursor(
+ String text, int contextStart, int contextEnd, int dir, int offset, int cursorOpt);
+
+ boolean hasShadowLayer();
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathDashPathEffectTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathDashPathEffectTest.java
new file mode 100644
index 000000000..c54f8ee77
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathDashPathEffectTest.java
@@ -0,0 +1,74 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.PathDashPathEffect;
+import android.graphics.RectF;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativePathDashPathEffectTest {
+ private static final int SQUARE = 10;
+ private static final int ADVANCE = 30;
+ private static final int WIDTH = 100;
+ private static final int HEIGHT = 100;
+
+ @Test
+ public void testPathDashPathEffect() {
+ Bitmap b = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
+ b.eraseColor(Color.BLACK);
+ PathDashPathEffect effect =
+ new PathDashPathEffect(shape(), ADVANCE, 0, PathDashPathEffect.Style.TRANSLATE);
+ Canvas canvas = new Canvas(b);
+ Paint p = new Paint();
+ p.setPathEffect(effect);
+ p.setColor(Color.RED);
+ canvas.drawPath(path(), p);
+
+ Bitmap expected = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
+ expected.eraseColor(Color.BLACK);
+ canvas = new Canvas(expected);
+ p = new Paint();
+ p.setColor(Color.RED);
+ RectF rect = new RectF(0, HEIGHT / 2 - SQUARE, 0, HEIGHT / 2 + SQUARE);
+ for (int i = 0; i <= WIDTH + SQUARE; i += ADVANCE) {
+ rect.left = i - SQUARE;
+ rect.right = i + SQUARE;
+ canvas.drawRect(rect, p);
+ }
+
+ int diffCount = 0;
+ for (int y = 0; y < HEIGHT; y++) {
+ for (int x = 0; x < WIDTH; x++) {
+ if (expected.getPixel(x, y) != b.getPixel(x, y)) {
+ diffCount += 1;
+ }
+ }
+ }
+ assertEquals(0, diffCount);
+ }
+
+ private static Path path() {
+ Path p = new Path();
+ p.moveTo(0, HEIGHT / 2);
+ p.lineTo(WIDTH, HEIGHT / 2);
+ return p;
+ }
+
+ private static Path shape() {
+ Path p = new Path();
+ p.addRect(new RectF(-SQUARE, -SQUARE, SQUARE, SQUARE), Direction.CCW);
+ return p;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathEffectTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathEffectTest.java
new file mode 100644
index 000000000..a9a6de1f8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathEffectTest.java
@@ -0,0 +1,19 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.PathEffect;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativePathEffectTest {
+
+ @Test
+ public void testConstructor() {
+ var unused = new PathEffect();
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathMeasureTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathMeasureTest.java
new file mode 100644
index 000000000..2f18f27e1
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathMeasureTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * This test is created from Android CTS tests:
+ *
+ * https://cs.android.com/android/platform/superproject/+/master:cts/tests/tests/graphics/src/android/graphics/cts/PathMeasureTest.java
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.PathMeasure;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativePathMeasureTest {
+ private PathMeasure pathMeasure;
+ private Path path;
+
+ @Before
+ public void setup() {
+ path = new Path();
+ pathMeasure = new PathMeasure();
+ }
+
+ @Test
+ public void testConstructor() {
+ pathMeasure = new PathMeasure();
+
+ // new the PathMeasure instance
+ Path path = new Path();
+ pathMeasure = new PathMeasure(path, true);
+
+ // new the PathMeasure instance
+ pathMeasure = new PathMeasure(path, false);
+ }
+
+ @Test
+ public void testGetPosTanArraysTooSmall() {
+ float distance = 1f;
+ float[] pos = {1f};
+ float[] tan = {1f};
+
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> {
+ pathMeasure.getPosTan(distance, pos, tan);
+ });
+ }
+
+ @Test
+ public void testGetPosTan() {
+ float distance = 1f;
+ float[] pos2 = {1f, 2f};
+ float[] tan2 = {1f, 3f};
+ assertFalse(pathMeasure.getPosTan(distance, pos2, tan2));
+
+ pathMeasure.setPath(path, true);
+ path.addRect(1f, 2f, 3f, 4f, Path.Direction.CW);
+ pathMeasure.setPath(path, true);
+ float[] pos3 = {1f, 2f, 3f, 4f};
+ float[] tan3 = {1f, 2f, 3f, 4f};
+ assertTrue(pathMeasure.getPosTan(0f, pos3, tan3));
+ }
+
+ @Test
+ public void testNextContour() {
+ assertFalse(pathMeasure.nextContour());
+ path.addRect(1, 2, 3, 4, Path.Direction.CW);
+ path.addRect(1, 2, 3, 4, Path.Direction.CW);
+ pathMeasure.setPath(path, true);
+ assertTrue(pathMeasure.nextContour());
+ assertFalse(pathMeasure.nextContour());
+ }
+
+ @Test
+ public void testGetLength() {
+ assertEquals(0f, pathMeasure.getLength(), 0.0f);
+ path.addRect(1, 2, 3, 4, Path.Direction.CW);
+ pathMeasure.setPath(path, true);
+ assertEquals(8.0f, pathMeasure.getLength(), 0.0f);
+ }
+
+ @Test
+ public void testIsClosed() {
+ Path circle = new Path();
+ circle.addCircle(0, 0, 1, Direction.CW);
+
+ PathMeasure measure = new PathMeasure(circle, false);
+ assertTrue(measure.isClosed());
+ measure.setPath(circle, true);
+ assertTrue(measure.isClosed());
+
+ Path line = new Path();
+ line.lineTo(5, 5);
+
+ measure.setPath(line, false);
+ assertFalse(measure.isClosed());
+ measure.setPath(line, true);
+ assertTrue(measure.isClosed());
+ }
+
+ @Test
+ public void testSetPath() {
+ pathMeasure.setPath(path, true);
+ // There is no getter and we can't obtain any status about it.
+ }
+
+ @Test
+ public void testGetSegment() {
+ assertEquals(0f, pathMeasure.getLength(), 0.0f);
+ path.addRect(1, 2, 3, 4, Path.Direction.CW);
+ pathMeasure.setPath(path, true);
+ assertEquals(8f, pathMeasure.getLength(), 0.0f);
+ Path dst = new Path();
+ assertTrue(pathMeasure.getSegment(0, pathMeasure.getLength(), dst, true));
+ assertFalse(pathMeasure.getSegment(pathMeasure.getLength(), 0, dst, true));
+ }
+
+ @Test
+ public void testGetMatrix() {
+ Matrix matrix = new Matrix();
+ assertFalse(pathMeasure.getMatrix(1f, matrix, PathMeasure.POSITION_MATRIX_FLAG));
+ matrix.setScale(1f, 2f);
+ path.addRect(1f, 2f, 3f, 4f, Path.Direction.CW);
+ pathMeasure.setPath(path, true);
+ assertTrue(pathMeasure.getMatrix(0f, matrix, PathMeasure.TANGENT_MATRIX_FLAG));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathParserTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathParserTest.java
new file mode 100644
index 000000000..dcf5de8e4
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathParserTest.java
@@ -0,0 +1,85 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Path;
+import android.util.PathParser;
+import android.util.PathParser.PathData;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativePathParserTest {
+
+ @Test
+ public void testCreatePathFromPathString() {
+ Path path = new Path();
+ PathData data = new PathData("M 275 80");
+ PathParser.createPathFromPathData(path, data);
+
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testCreatePathFromPathData() {
+ Path path = new Path();
+ PathData original = new PathData("M 275 80");
+ PathData data = new PathData(original);
+ PathParser.createPathFromPathData(path, data);
+
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testCreatePathFromEmptyPathData() {
+ Path path = new Path();
+ PathData data = new PathData();
+ PathParser.createPathFromPathData(path, data);
+
+ assertTrue(path.isEmpty());
+ }
+
+ @Test
+ public void testCreatePathFromEmptyPathDataWithSetPathData() {
+ Path path = new Path();
+ PathData original = new PathData("M 275 80");
+ PathData data = new PathData();
+ data.setPathData(original);
+ PathParser.createPathFromPathData(path, data);
+
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testCreatePathFromPathParserPathString() {
+ Path path = PathParser.createPathFromPathData("M 275 80");
+
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testInterpolatePathData() {
+ Path path = new Path();
+ PathData pathData = new PathData();
+ PathData from = new PathData("M 100 100");
+ PathData to = new PathData("M 200 200");
+ PathParser.interpolatePathData(pathData, from, to, 0.5f);
+
+ PathParser.createPathFromPathData(path, pathData);
+
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testCanMorph() {
+ PathData data1 = new PathData("M 275 80");
+ PathData data2 = new PathData("M 275 80");
+
+ assertTrue(PathParser.canMorph(data1, data2));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathTest.java
new file mode 100644
index 000000000..7a0d11c67
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePathTest.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * These tests are taken from
+ * https://cs.android.com/android/platform/superproject/+/master:cts/tests/tests/graphics/src/android/graphics/cts/PathTest.java
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.RectF;
+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;
+import org.robolectric.shadows.ShadowPath;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativePathTest {
+
+ // Test constants
+ private static final float LEFT = 10.0f;
+ private static final float RIGHT = 50.0f;
+ private static final float TOP = 10.0f;
+ private static final float BOTTOM = 50.0f;
+ private static final float XCOORD = 40.0f;
+ private static final float YCOORD = 40.0f;
+
+ @Test
+ public void testAddRect1() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF rect = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ path.addRect(rect, Path.Direction.CW);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testAddRect2() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.addRect(LEFT, TOP, RIGHT, BOTTOM, Path.Direction.CW);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testMoveTo() {
+ Path path = new Path();
+ path.moveTo(10.0f, 10.0f);
+ }
+
+ @Test
+ public void testAccessFillType() {
+ // set the expected value
+ Path.FillType expected1 = Path.FillType.EVEN_ODD;
+ Path.FillType expected2 = Path.FillType.INVERSE_EVEN_ODD;
+ Path.FillType expected3 = Path.FillType.INVERSE_WINDING;
+ Path.FillType expected4 = Path.FillType.WINDING;
+
+ // new the Path instance
+ Path path = new Path();
+ // set FillType by {@link Path#setFillType(FillType)}
+ path.setFillType(Path.FillType.EVEN_ODD);
+ assertEquals(expected1, path.getFillType());
+ path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ assertEquals(expected2, path.getFillType());
+ path.setFillType(Path.FillType.INVERSE_WINDING);
+ assertEquals(expected3, path.getFillType());
+ path.setFillType(Path.FillType.WINDING);
+ assertEquals(expected4, path.getFillType());
+ }
+
+ @Test
+ public void testRQuadTo() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.rQuadTo(5.0f, 5.0f, 10.0f, 10.0f);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testTransform1() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ Path dst = new Path();
+ addRectToPath(path);
+ path.transform(new Matrix(), dst);
+ assertFalse(dst.isEmpty());
+ }
+
+ @Test
+ public void testLineTo() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.lineTo(XCOORD, YCOORD);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testClose() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ addRectToPath(path);
+ path.close();
+ }
+
+ @Test
+ public void testQuadTo() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.quadTo(20.0f, 20.0f, 40.0f, 40.0f);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testAddCircle() {
+ // new the Path instance
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.addCircle(XCOORD, YCOORD, 10.0f, Path.Direction.CW);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testArcTo1() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF oval = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ path.arcTo(oval, 0.0f, 30.0f, true);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testArcTo2() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF oval = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ path.arcTo(oval, 0.0f, 30.0f);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testComputeBounds1() {
+ RectF expected = new RectF(0.0f, 0.0f, 0.0f, 0.0f);
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF bounds = new RectF();
+ path.computeBounds(bounds, true);
+ assertEquals(expected.width(), bounds.width(), 0.0f);
+ assertEquals(expected.height(), bounds.height(), 0.0f);
+ path.computeBounds(bounds, false);
+ assertEquals(expected.width(), bounds.width(), 0.0f);
+ assertEquals(expected.height(), bounds.height(), 0.0f);
+ }
+
+ @Test
+ public void testComputeBounds2() {
+ RectF expected = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF bounds = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ path.addRect(bounds, Path.Direction.CW);
+ path.computeBounds(bounds, true);
+ assertEquals(expected.width(), bounds.width(), 0.0f);
+ assertEquals(expected.height(), bounds.height(), 0.0f);
+ path.computeBounds(bounds, false);
+ assertEquals(expected.width(), bounds.width(), 0.0f);
+ assertEquals(expected.height(), bounds.height(), 0.0f);
+ }
+
+ @Test
+ public void testSetLastPoint() {
+ Path path = new Path();
+ path.setLastPoint(10.0f, 10.0f);
+ }
+
+ @Test
+ public void testRLineTo() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.rLineTo(10.0f, 10.0f);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testIsEmpty() {
+
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ addRectToPath(path);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testRewind() {
+ Path.FillType expected = Path.FillType.EVEN_ODD;
+
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ addRectToPath(path);
+ path.rewind();
+ path.setFillType(Path.FillType.EVEN_ODD);
+ assertTrue(path.isEmpty());
+ assertEquals(expected, path.getFillType());
+ }
+
+ @Test
+ public void testAddOval() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF oval = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ path.addOval(oval, Path.Direction.CW);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testIsRect() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ addRectToPath(path);
+ }
+
+ @Test
+ public void testAddPath1() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ Path src = new Path();
+ addRectToPath(src);
+ path.addPath(src, 10.0f, 10.0f);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testAddPath2() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ Path src = new Path();
+ addRectToPath(src);
+ path.addPath(src);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testAddPath3() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ Path src = new Path();
+ addRectToPath(src);
+ Matrix matrix = new Matrix();
+ path.addPath(src, matrix);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testAddRoundRect1() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF rect = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ path.addRoundRect(rect, XCOORD, YCOORD, Path.Direction.CW);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testAddRoundRect2() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF rect = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ float[] radii = new float[8];
+ for (int i = 0; i < 8; i++) {
+ radii[i] = 10.0f + i * 5.0f;
+ }
+ path.addRoundRect(rect, radii, Path.Direction.CW);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testIsConvex1() {
+ Path path = new Path();
+ path.addRect(0, 0, 100, 10, Path.Direction.CW);
+ assertTrue(path.isConvex());
+
+ path.addRect(0, 0, 10, 100, Path.Direction.CW);
+ assertFalse(path.isConvex()); // path is concave
+ }
+
+ @Test
+ public void testIsConvex2() {
+ Path path = new Path();
+ path.addRect(0, 0, 40, 40, Path.Direction.CW);
+ assertTrue(path.isConvex());
+
+ path.addRect(10, 10, 30, 30, Path.Direction.CCW);
+ assertFalse(path.isConvex()); // path has hole, isn't convex
+ }
+
+ @Test
+ public void testIsConvex3() {
+ Path path = new Path();
+ path.addRect(0, 0, 10, 10, Path.Direction.CW);
+ assertTrue(path.isConvex());
+
+ path.addRect(0, 20, 10, 10, Path.Direction.CW);
+ assertFalse(path.isConvex()); // path isn't one convex shape
+ }
+
+ @Test
+ public void testIsInverseFillType() {
+ Path path = new Path();
+ assertFalse(path.isInverseFillType());
+ path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ assertTrue(path.isInverseFillType());
+ }
+
+ @Test
+ public void testOffset1() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ addRectToPath(path);
+ Path dst = new Path();
+ path.offset(XCOORD, YCOORD, dst);
+ assertFalse(dst.isEmpty());
+ }
+
+ @Test
+ public void testCubicTo() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.cubicTo(10.0f, 10.0f, 20.0f, 20.0f, 30.0f, 30.0f);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testReset() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ Path path1 = new Path();
+ addRectToPath(path1);
+ path.set(path1);
+ assertFalse(path.isEmpty());
+ path.reset();
+ assertTrue(path.isEmpty());
+ }
+
+ @Test
+ public void testToggleInverseFillType() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.toggleInverseFillType();
+ assertTrue(path.isInverseFillType());
+ }
+
+ @Test
+ public void testAddArc() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ RectF oval = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ path.addArc(oval, 0.0f, 30.0f);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testRCubicTo() {
+ Path path = new Path();
+ assertTrue(path.isEmpty());
+ path.rCubicTo(10.0f, 10.0f, 11.0f, 11.0f, 12.0f, 12.0f);
+ assertFalse(path.isEmpty());
+ }
+
+ @Test
+ public void testApproximate_lowError() {
+ assertThrows(IllegalArgumentException.class, () -> new Path().approximate(-0.1f));
+ }
+
+ @Test
+ public void testApproximate_rect_cw() {
+ Path path = new Path();
+ path.addRect(0, 0, 100, 100, Path.Direction.CW);
+ assertArrayEquals(
+ new float[] {
+ 0, 0, 0, 0.25f, 100, 0, 0.50f, 100, 100, 0.75f, 0, 100, 1, 0, 0,
+ },
+ path.approximate(1f),
+ 0);
+ }
+
+ @Test
+ public void testApproximate_rect_ccw() {
+ Path path = new Path();
+ path.addRect(0, 0, 100, 100, Path.Direction.CCW);
+ assertArrayEquals(
+ new float[] {
+ 0, 0, 0, 0.25f, 0, 100, 0.50f, 100, 100, 0.75f, 100, 0, 1, 0, 0,
+ },
+ path.approximate(1f),
+ 0);
+ }
+
+ @Test
+ public void testApproximate_empty() {
+ Path path = new Path();
+ assertArrayEquals(
+ new float[] {
+ 0, 0, 0,
+ 1, 0, 0,
+ },
+ path.approximate(0.5f),
+ 0);
+ }
+
+ @Test
+ public void testApproximate_circle() {
+ Path path = new Path();
+ path.addCircle(0, 0, 50, Path.Direction.CW);
+ assertTrue(path.approximate(0.25f).length > 20);
+ }
+
+ @Test
+ public void legacyShadowPathAPIs_notSupported() {
+ Path path = new Path();
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> {
+ ((ShadowPath) Shadow.extract(path)).getPoints();
+ });
+ }
+
+ private void addRectToPath(Path path) {
+ RectF rect = new RectF(LEFT, TOP, RIGHT, BOTTOM);
+ path.addRect(rect, Path.Direction.CW);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePictureTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePictureTest.java
new file mode 100644
index 000000000..d66461a28
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePictureTest.java
@@ -0,0 +1,153 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Picture;
+import android.graphics.Rect;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativePictureTest {
+
+ private static final int TEST_WIDTH = 4; // must be >= 2
+ private static final int TEST_HEIGHT = 3; // must >= 2
+
+ private final Rect mClipRect = new Rect(0, 0, 2, 2);
+
+ // This method tests out some edge cases w.r.t. Picture creation.
+ // In particular, this test verifies that, in the following situations,
+ // the created picture (effectively) has balanced saves and restores:
+ // - copy constructed picture from actively recording picture
+ // - actively recording picture after draw call
+ @Test
+ public void testSaveRestoreBalance() {
+ Picture original = new Picture();
+ Canvas canvas = original.beginRecording(TEST_WIDTH, TEST_HEIGHT);
+ assertNotNull(canvas);
+ createImbalance(canvas);
+
+ int expectedSaveCount = canvas.getSaveCount();
+
+ Picture copy = new Picture(original);
+ verifyBalance(copy);
+
+ assertEquals(expectedSaveCount, canvas.getSaveCount());
+
+ Bitmap bitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+ Canvas drawDest = new Canvas(bitmap);
+ original.draw(drawDest);
+ verifyBalance(original);
+ }
+
+ // Add an extra save with a transform and clip
+ private void createImbalance(Canvas canvas) {
+ canvas.save();
+ canvas.clipRect(mClipRect);
+ canvas.translate(1.0f, 1.0f);
+ Paint paint = new Paint();
+ paint.setColor(Color.GREEN);
+ canvas.drawRect(0, 0, 10, 10, paint);
+ }
+
+ private void verifyBalance(Picture picture) {
+ Bitmap bitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ int beforeSaveCount = canvas.getSaveCount();
+
+ final Matrix beforeMatrix = canvas.getMatrix();
+
+ Rect beforeClip = new Rect();
+ assertTrue(canvas.getClipBounds(beforeClip));
+
+ canvas.drawPicture(picture);
+
+ assertEquals(beforeSaveCount, canvas.getSaveCount());
+
+ assertTrue(beforeMatrix.equals(canvas.getMatrix()));
+
+ Rect afterClip = new Rect();
+
+ assertTrue(canvas.getClipBounds(afterClip));
+ assertEquals(beforeClip, afterClip);
+ }
+
+ @Test
+ public void testPicture() {
+ Picture picture = new Picture();
+
+ Canvas canvas = picture.beginRecording(TEST_WIDTH, TEST_HEIGHT);
+ assertNotNull(canvas);
+ drawPicture(canvas);
+ picture.endRecording();
+
+ Bitmap bitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+ canvas = new Canvas(bitmap);
+ picture.draw(canvas);
+ verifySize(picture);
+ verifyBitmap(bitmap);
+
+ Picture pic = new Picture(picture);
+ bitmap = Bitmap.createBitmap(TEST_WIDTH, TEST_HEIGHT, Bitmap.Config.ARGB_8888);
+ canvas = new Canvas(bitmap);
+ pic.draw(canvas);
+ verifySize(pic);
+ verifyBitmap(bitmap);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ @Config(minSdk = P) // This did not exist in O or O_MR1
+ public void testBeginRecordingTwice() {
+ Picture picture = new Picture();
+ picture.beginRecording(10, 10);
+ picture.beginRecording(10, 10);
+ }
+
+ private void verifySize(Picture picture) {
+ assertEquals(TEST_WIDTH, picture.getWidth());
+ assertEquals(TEST_HEIGHT, picture.getHeight());
+ }
+
+ private void drawPicture(Canvas canvas) {
+ Paint paint = new Paint();
+ // GREEN rectangle covering the entire canvas
+ paint.setColor(Color.GREEN);
+ paint.setStyle(Style.FILL);
+ paint.setAntiAlias(false);
+ canvas.drawRect(0, 0, TEST_WIDTH, TEST_HEIGHT, paint);
+ // horizontal red line starting from (0,0); overwrites first line of the rectangle
+ paint.setColor(Color.RED);
+ canvas.drawLine(0, 0, TEST_WIDTH, 0, paint);
+ // overwrite (0,0) with a blue dot
+ paint.setColor(Color.BLUE);
+ canvas.drawPoint(0, 0, paint);
+ }
+
+ private void verifyBitmap(Bitmap bitmap) {
+ // first pixel is BLUE, rest of the line is RED
+ assertEquals(Color.BLUE, bitmap.getPixel(0, 0));
+ for (int x = 1; x < TEST_WIDTH; x++) {
+ assertEquals(Color.RED, bitmap.getPixel(x, 0));
+ }
+ // remaining lines are all green
+ for (int y = 1; y < TEST_HEIGHT; y++) {
+ for (int x = 0; x < TEST_WIDTH; x++) {
+ assertEquals(Color.GREEN, bitmap.getPixel(x, y));
+ }
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePorterDuffColorFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePorterDuffColorFilterTest.java
new file mode 100644
index 000000000..f95d31510
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePorterDuffColorFilterTest.java
@@ -0,0 +1,77 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativePorterDuffColorFilterTest {
+ private static final int TOLERANCE = 5;
+
+ @Test
+ public void testPorterDuffColorFilter() {
+ int width = 100;
+ int height = 100;
+ Bitmap b1 = Bitmap.createBitmap(width / 2, height, Bitmap.Config.ARGB_8888);
+ b1.eraseColor(Color.RED);
+ Bitmap b2 = Bitmap.createBitmap(width, height / 2, Bitmap.Config.ARGB_8888);
+ b2.eraseColor(Color.BLUE);
+
+ Bitmap target = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ target.eraseColor(Color.TRANSPARENT);
+ Canvas canvas = new Canvas(target);
+ // semi-transparent green
+ int filterColor = Color.argb(0x80, 0, 0xFF, 0);
+ PorterDuffColorFilter filter = new PorterDuffColorFilter(filterColor, PorterDuff.Mode.SRC);
+ Paint p = new Paint();
+ canvas.drawBitmap(b1, 0, 0, p);
+ p.setColorFilter(filter);
+ canvas.drawBitmap(b2, 0, height / 2, p);
+ assertEquals(Color.RED, target.getPixel(width / 4, height / 4));
+ int lowerLeft = target.getPixel(width / 4, height * 3 / 4);
+ assertEquals(0x80, Color.red(lowerLeft), TOLERANCE);
+ assertEquals(0x80, Color.green(lowerLeft), TOLERANCE);
+ int lowerRight = target.getPixel(width * 3 / 4, height * 3 / 4);
+ assertEquals(filterColor, lowerRight);
+
+ target.eraseColor(Color.BLACK);
+ filter = new PorterDuffColorFilter(filterColor, PorterDuff.Mode.DST);
+ p.setColorFilter(null);
+ canvas.drawBitmap(b1, 0, 0, p);
+ p.setColorFilter(filter);
+ canvas.drawBitmap(b2, 0, height / 2, p);
+ assertEquals(Color.RED, target.getPixel(width / 4, height / 4));
+ assertEquals(Color.BLUE, target.getPixel(width / 4, height * 3 / 4));
+ assertEquals(Color.BLUE, target.getPixel(width * 3 / 4, height * 3 / 4));
+
+ target.eraseColor(Color.BLACK);
+ filter = new PorterDuffColorFilter(Color.GREEN, PorterDuff.Mode.SCREEN);
+ p.setColorFilter(null);
+ canvas.drawBitmap(b1, 0, 0, p);
+ p.setColorFilter(filter);
+ canvas.drawBitmap(b2, 0, height / 2, p);
+ assertEquals(Color.RED, target.getPixel(width / 4, height / 4));
+ assertEquals(Color.CYAN, target.getPixel(width / 4, height * 3 / 4));
+ assertEquals(Color.CYAN, target.getPixel(width * 3 / 4, height * 3 / 4));
+ }
+
+ @Test
+ public void legacy_shadow_apis() {
+ PorterDuffColorFilter filter = new PorterDuffColorFilter(Color.GREEN, PorterDuff.Mode.SRC);
+ assertThat(shadowOf(filter).getColor()).isEqualTo(Color.GREEN);
+ assertThat(shadowOf(filter).getMode()).isEqualTo(PorterDuff.Mode.SRC);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePropertyValuesHolderTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePropertyValuesHolderTest.java
new file mode 100644
index 000000000..f47db1d94
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativePropertyValuesHolderTest.java
@@ -0,0 +1,55 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+
+import android.animation.PropertyValuesHolder;
+import android.app.Instrumentation;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativePropertyValuesHolderTest {
+
+ private Instrumentation instrumentation;
+ private float startY;
+ private float endY;
+ private String property;
+
+ @Before
+ public void setup() {
+ instrumentation = InstrumentationRegistry.getInstrumentation();
+ instrumentation.setInTouchMode(false);
+ property = "y";
+ startY = 0;
+ endY = 10;
+ }
+
+ @Test
+ public void testGetPropertyName() {
+ float[] values = {startY, endY};
+ PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat(property, values);
+ assertEquals(property, pVHolder.getPropertyName());
+ }
+
+ @Test
+ public void testSetPropertyName() {
+ float[] values = {startY, endY};
+ PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat("", values);
+ pVHolder.setPropertyName(property);
+ assertEquals(property, pVHolder.getPropertyName());
+ }
+
+ @Test
+ public void testClone() {
+ float[] values = {startY, endY};
+ PropertyValuesHolder pVHolder = PropertyValuesHolder.ofFloat(property, values);
+ PropertyValuesHolder cloneHolder = pVHolder.clone();
+ assertEquals(pVHolder.getPropertyName(), cloneHolder.getPropertyName());
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRadialGradientTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRadialGradientTest.java
new file mode 100644
index 000000000..b5c6ea02c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRadialGradientTest.java
@@ -0,0 +1,341 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorSpace;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.RadialGradient;
+import android.graphics.Shader.TileMode;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.function.Function;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeRadialGradientTest {
+ @Test
+ public void testZeroScaleMatrix() {
+ RadialGradient gradient =
+ new RadialGradient(0.5f, 0.5f, 1, Color.RED, Color.BLUE, TileMode.CLAMP);
+
+ Matrix m = new Matrix();
+ m.setScale(0, 0);
+ gradient.setLocalMatrix(m);
+
+ Bitmap bitmap = Bitmap.createBitmap(3, 1, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.BLACK);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setShader(gradient);
+ canvas.drawPaint(paint);
+
+ ColorUtils.verifyColor(Color.BLACK, bitmap.getPixel(0, 0), 1);
+ ColorUtils.verifyColor(Color.BLACK, bitmap.getPixel(1, 0), 1);
+ ColorUtils.verifyColor(Color.BLACK, bitmap.getPixel(2, 0), 1);
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testColorLong() {
+ ColorSpace p3 = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
+ long red = Color.pack(1, 0, 0, 1, p3);
+ long blue = Color.pack(0, 0, 1, 1, p3);
+ RadialGradient gradient = new RadialGradient(50, 50, 25, red, blue, TileMode.CLAMP);
+
+ Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.RGBA_F16);
+ bitmap.eraseColor(Color.TRANSPARENT);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setShader(gradient);
+ canvas.drawPaint(paint);
+
+ final ColorSpace bitmapColorSpace = bitmap.getColorSpace();
+ Function<Long, Color> convert =
+ (l) -> {
+ return Color.valueOf(Color.convert(l, bitmapColorSpace));
+ };
+
+ final Color centerColor = bitmap.getColor(50, 50);
+ ColorUtils.verifyColor("Center color should be red!", convert.apply(red), centerColor, 0.034f);
+ Color blueColor = convert.apply(blue);
+ for (Point p :
+ new Point[] {
+ new Point(0, 0),
+ new Point(50, 0),
+ new Point(99, 0),
+ new Point(0, 50),
+ new Point(0, 99),
+ new Point(99, 0),
+ new Point(99, 50),
+ new Point(99, 99)
+ }) {
+ ColorUtils.verifyColor(
+ "Edge point " + p + " should be blue", blueColor, bitmap.getColor(p.x, p.y), .001f);
+ }
+
+ final double[] negativeOneAndOne = new double[] {-1, 1};
+ Color lastColor = centerColor;
+ Point lastPoint = new Point(0, 0);
+ // On several different radii, verify that colors trend from red to blue.
+ for (double radius = 4; radius < 25; radius += 4) {
+ // These correspond to the first point we check at a given radius.
+ Color currentColor = null;
+ Point currentPoint = null;
+ for (double angle = 0; angle <= Math.PI / 2.0; angle += Math.PI / 8.0) {
+ double dx = Math.cos(angle) * radius;
+ double dy = Math.sin(angle) * radius;
+ for (double nx : negativeOneAndOne) {
+ for (double ny : negativeOneAndOne) {
+ int x = 50 + (int) (nx * dx);
+ int y = 50 + (int) (ny * dy);
+ Color c = bitmap.getColor(x, y);
+ if (currentColor == null) {
+ currentColor = c;
+ currentPoint = new Point(x, y);
+ assertTrue(
+ "Outer "
+ + currentPoint
+ + " ("
+ + currentColor
+ + ") should be less red than inner "
+ + lastPoint
+ + " ("
+ + lastColor
+ + ")",
+ currentColor.red() < lastColor.red());
+ assertTrue(
+ "Outer "
+ + currentPoint
+ + " ("
+ + currentColor
+ + ") should be more blue than inner "
+ + lastPoint
+ + " ("
+ + lastColor
+ + ")",
+ currentColor.blue() > lastColor.blue());
+ } else {
+ ColorUtils.verifyColor(
+ "Point(" + x + ", " + y + ") should match " + currentPoint,
+ currentColor,
+ c,
+ .08f);
+ }
+ }
+ }
+ }
+
+ lastColor = currentColor;
+ lastPoint = currentPoint;
+ }
+ }
+
+ @Test
+ public void testNullColorInts() {
+ int[] colors = null;
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ RadialGradient unused = new RadialGradient(0.5f, 0.5f, 1, colors, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testNullColorLongs() {
+ long[] colors = null;
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ RadialGradient unused = new RadialGradient(0.5f, 0.5f, 1, colors, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ public void testNoColorInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 1, new int[0], null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testNoColorLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 1, new long[0], null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ public void testOneColorInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 1, new int[1], null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testOneColorLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 1, new long[1], null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchColorLongs() {
+ long[] colors = new long[2];
+ colors[0] = Color.pack(Color.BLUE);
+ colors[1] = Color.pack(.5f, .5f, .5f, 1.0f, ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused = new RadialGradient(0.5f, 0.5f, 1, colors, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchColorLongs2() {
+ long color0 = Color.pack(Color.BLUE);
+ long color1 = Color.pack(.5f, .5f, .5f, 1.0f, ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused = new RadialGradient(0.5f, 0.5f, 1, color0, color1, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchPositionsInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 1, new int[2], new float[3], TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchPositionsLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 1, new long[2], new float[3], TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLongs() {
+ long[] colors = new long[2];
+ colors[0] = -1L;
+ colors[0] = -2L;
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused = new RadialGradient(0.5f, 0.5f, 1, colors, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLong() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 1, -1L, Color.pack(Color.RED), TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLong2() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 1, Color.pack(Color.RED), -1L, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testZeroRadius() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(0.5f, 0.5f, 0, Color.RED, Color.BLUE, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testZeroRadiusArray() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(
+ 0.5f, 0.5f, 0, new int[] {Color.RED, Color.BLUE}, null, TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testZeroRadiusLong() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(
+ 0.5f, 0.5f, 0, Color.pack(Color.RED), Color.pack(Color.BLUE), TileMode.CLAMP);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testZeroRadiusLongArray() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ RadialGradient unused =
+ new RadialGradient(
+ 0.5f,
+ 0.5f,
+ 0,
+ new long[] {Color.pack(Color.RED), Color.pack(Color.BLUE)},
+ null,
+ TileMode.CLAMP);
+ });
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRegionIteratorTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRegionIteratorTest.java
new file mode 100644
index 000000000..aa1a81d21
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRegionIteratorTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * These tests are taken from
+ * https://cs.android.com/android/platform/superproject/+/master:cts/tests/tests/graphics/src/android/graphics/cts/RegionTest.java
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeRegionIteratorTest {
+ @Test
+ public void testIterateRegion() {
+ final Region region = new Region(1, 2, 3, 4);
+ final RegionIterator it = new RegionIterator(region);
+ final Rect rect = new Rect();
+ while (it.next(rect)) {
+ // Unused
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRegionTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRegionTest.java
new file mode 100644
index 000000000..832dcc05f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRegionTest.java
@@ -0,0 +1,1531 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * These tests are taken from
+ * https://cs.android.com/android/platform/superproject/+/master:cts/tests/tests/graphics/src/android/graphics/cts/RegionTest.java
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Region;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeRegionTest {
+ // DIFFERENCE
+ private static final int[][] DIFFERENCE_WITH1 = {
+ {0, 0}, {4, 4}, {10, 10}, {19, 19}, {19, 0}, {10, 4}, {4, 10}, {0, 19}
+ };
+ private static final int[][] DIFFERENCE_WITHOUT1 = {{5, 5}, {9, 9}, {9, 5}, {5, 9}};
+
+ private static final int[][] DIFFERENCE_WITH2 = {
+ {0, 0}, {19, 0}, {9, 9}, {19, 9}, {0, 19}, {9, 19}
+ };
+ private static final int[][] DIFFERENCE_WITHOUT2 = {
+ {10, 10}, {19, 10}, {10, 19}, {19, 19}, {29, 10}, {29, 29}, {10, 29}
+ };
+
+ private static final int[][] DIFFERENCE_WITH3 = {{0, 0}, {19, 0}, {0, 19}, {19, 19}};
+ private static final int[][] DIFFERENCE_WITHOUT3 = {{40, 40}, {40, 59}, {59, 40}, {59, 59}};
+
+ // INTERSECT
+ private static final int[][] INTERSECT_WITH1 = {{5, 5}, {9, 9}, {9, 5}, {5, 9}};
+ private static final int[][] INTERSECT_WITHOUT1 = {
+ {0, 0}, {2, 2}, {4, 4}, {10, 10}, {19, 19}, {19, 0}, {10, 4}, {4, 10}, {0, 19}
+ };
+
+ private static final int[][] INTERSECT_WITH2 = {{10, 10}, {19, 10}, {10, 19}, {19, 19}};
+ private static final int[][] INTERSECT_WITHOUT2 = {
+ {0, 0}, {19, 0}, {9, 9}, {19, 9}, {0, 19}, {9, 19}, {29, 10}, {29, 29}, {10, 29}
+ };
+
+ // UNION
+ private static final int[][] UNION_WITH1 = {
+ {0, 0}, {2, 2}, {4, 4}, {6, 6}, {10, 10}, {19, 19}, {19, 0}, {10, 4}, {4, 10}, {0, 19}, {5, 5},
+ {9, 9}, {9, 5}, {5, 9}
+ };
+ private static final int[][] UNION_WITHOUT1 = {{0, 20}, {20, 20}, {20, 0}};
+
+ private static final int[][] UNION_WITH2 = {
+ {0, 0}, {2, 2}, {19, 0}, {9, 9}, {19, 9}, {0, 19}, {9, 19}, {21, 21}, {10, 10}, {19, 10},
+ {10, 19}, {19, 19}, {29, 10}, {29, 29}, {10, 29}
+ };
+ private static final int[][] UNION_WITHOUT2 = {
+ {0, 29}, {0, 20}, {9, 29}, {9, 20},
+ {29, 0}, {20, 0}, {29, 9}, {20, 9}
+ };
+
+ private static final int[][] UNION_WITH3 = {
+ {0, 0}, {2, 2}, {19, 0}, {0, 19}, {19, 19},
+ {40, 40}, {41, 41}, {40, 59}, {59, 40}, {59, 59}
+ };
+ private static final int[][] UNION_WITHOUT3 = {{20, 20}, {39, 39}};
+
+ // XOR
+ private static final int[][] XOR_WITH1 = {
+ {0, 0}, {2, 2}, {4, 4}, {10, 10}, {19, 19}, {19, 0}, {10, 4}, {4, 10}, {0, 19}
+ };
+ private static final int[][] XOR_WITHOUT1 = {{5, 5}, {6, 6}, {9, 9}, {9, 5}, {5, 9}};
+
+ private static final int[][] XOR_WITH2 = {
+ {0, 0}, {2, 2}, {19, 0}, {9, 9}, {19, 9}, {0, 19}, {9, 19}, {21, 21}, {29, 10}, {10, 29},
+ {20, 10}, {10, 20}, {20, 20}, {29, 29}
+ };
+ private static final int[][] XOR_WITHOUT2 = {{10, 10}, {11, 11}, {19, 10}, {10, 19}, {19, 19}};
+
+ private static final int[][] XOR_WITH3 = {
+ {0, 0}, {2, 2}, {19, 0}, {0, 19}, {19, 19},
+ {40, 40}, {41, 41}, {40, 59}, {59, 40}, {59, 59}
+ };
+ private static final int[][] XOR_WITHOUT3 = {{20, 20}, {39, 39}};
+
+ // REVERSE_DIFFERENCE
+ private static final int[][] REVERSE_DIFFERENCE_WITH2 = {
+ {29, 10}, {10, 29}, {20, 10}, {10, 20}, {20, 20}, {29, 29}, {21, 21}
+ };
+ private static final int[][] REVERSE_DIFFERENCE_WITHOUT2 = {
+ {0, 0}, {19, 0}, {0, 19}, {19, 19}, {2, 2}, {11, 11}
+ };
+
+ private static final int[][] REVERSE_DIFFERENCE_WITH3 = {
+ {40, 40}, {40, 59}, {59, 40}, {59, 59}, {41, 41}
+ };
+ private static final int[][] REVERSE_DIFFERENCE_WITHOUT3 = {
+ {0, 0}, {19, 0}, {0, 19}, {19, 19}, {20, 20}, {39, 39}, {2, 2}
+ };
+
+ private Region region;
+
+ private void verifyPointsInsideRegion(int[][] area) {
+ for (int i = 0; i < area.length; i++) {
+ assertTrue(region.contains(area[i][0], area[i][1]));
+ }
+ }
+
+ private void verifyPointsOutsideRegion(int[][] area) {
+ for (int i = 0; i < area.length; i++) {
+ assertFalse(region.contains(area[i][0], area[i][1]));
+ }
+ }
+
+ @Before
+ public void setup() {
+ region = new Region();
+ }
+
+ @Test
+ public void testConstructor() {
+ // We don't actually care about the result of quickContains in this function (tested later).
+ // We call it because in robolectric native runtime, it's not static and so if the constructor
+ // is set up incorrectly, it will crash trying to get the instance from mNativeRegion.
+ Rect rect = new Rect();
+
+ // Test Region()
+ Region defaultRegion = new Region();
+ defaultRegion.quickContains(rect);
+
+ // Test Region(Region)
+ Region oriRegion = new Region();
+ Region copyRegion = new Region(oriRegion);
+ copyRegion.quickContains(rect);
+
+ // Test Region(Rect)
+ Region rectRegion = new Region(rect);
+ rectRegion.quickContains(rect);
+
+ // Test Region(int, int, int, int)
+ Region intRegion = new Region(0, 0, 100, 100);
+ intRegion.quickContains(rect);
+ }
+
+ @Test
+ public void testSet1() {
+ Rect rect = new Rect(1, 2, 3, 4);
+ Region oriRegion = new Region(rect);
+ assertTrue(region.set(oriRegion));
+ assertEquals(1, region.getBounds().left);
+ assertEquals(2, region.getBounds().top);
+ assertEquals(3, region.getBounds().right);
+ assertEquals(4, region.getBounds().bottom);
+ }
+
+ @Test
+ public void testSet2() {
+ Rect rect = new Rect(1, 2, 3, 4);
+ assertTrue(region.set(rect));
+ assertEquals(1, region.getBounds().left);
+ assertEquals(2, region.getBounds().top);
+ assertEquals(3, region.getBounds().right);
+ assertEquals(4, region.getBounds().bottom);
+ }
+
+ @Test
+ public void testSet3() {
+ assertTrue(region.set(1, 2, 3, 4));
+ assertEquals(1, region.getBounds().left);
+ assertEquals(2, region.getBounds().top);
+ assertEquals(3, region.getBounds().right);
+ assertEquals(4, region.getBounds().bottom);
+ }
+
+ @Test
+ public void testIsRect() {
+ assertFalse(region.isRect());
+ region = new Region(1, 2, 3, 4);
+ assertTrue(region.isRect());
+ }
+
+ @Test
+ public void testIsComplex() {
+ // Region is empty
+ assertFalse(region.isComplex());
+
+ // Only one rectangle
+ region = new Region();
+ region.set(1, 2, 3, 4);
+ assertFalse(region.isComplex());
+
+ // More than one rectangle
+ region = new Region();
+ region.set(1, 1, 2, 2);
+ region.union(new Rect(3, 3, 5, 5));
+ assertTrue(region.isComplex());
+ }
+
+ @Test
+ public void testQuickContains1() {
+ Rect rect = new Rect(1, 2, 3, 4);
+ // This region not contains expected rectangle.
+ assertFalse(region.quickContains(rect));
+ region.set(rect);
+ // This region contains only one rectangle and it is the expected one.
+ assertTrue(region.quickContains(rect));
+ region.set(5, 6, 7, 8);
+ // This region contains more than one rectangle.
+ assertFalse(region.quickContains(rect));
+ }
+
+ @Test
+ public void testQuickContains2() {
+ // This region not contains expected rectangle.
+ assertFalse(region.quickContains(1, 2, 3, 4));
+ region.set(1, 2, 3, 4);
+ // This region contains only one rectangle and it is the expected one.
+ assertTrue(region.quickContains(1, 2, 3, 4));
+ region.set(5, 6, 7, 8);
+ // This region contains more than one rectangle.
+ assertFalse(region.quickContains(1, 2, 3, 4));
+ }
+
+ @Test
+ public void testUnion() {
+ Rect rect1 = new Rect();
+ Rect rect2 = new Rect(0, 0, 20, 20);
+ Rect rect3 = new Rect(5, 5, 10, 10);
+ Rect rect4 = new Rect(10, 10, 30, 30);
+ Rect rect5 = new Rect(40, 40, 60, 60);
+
+ // union (inclusive-or) the two regions
+ region.set(rect2);
+ // union null rectangle
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.union(rect1));
+ assertTrue(region.contains(6, 6));
+
+ // 1. union rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.union(rect3));
+ verifyPointsInsideRegion(UNION_WITH1);
+ verifyPointsOutsideRegion(UNION_WITHOUT1);
+
+ // 2. union rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.union(rect4));
+ verifyPointsInsideRegion(UNION_WITH2);
+ verifyPointsOutsideRegion(UNION_WITHOUT2);
+
+ // 3. union rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.union(rect5));
+ verifyPointsInsideRegion(UNION_WITH3);
+ verifyPointsOutsideRegion(UNION_WITHOUT3);
+ }
+
+ @Test
+ public void testContains() {
+ region.set(2, 2, 5, 5);
+ // Not contain (1, 1).
+ assertFalse(region.contains(1, 1));
+
+ // Test point inside this region.
+ assertTrue(region.contains(3, 3));
+
+ // Test left-top corner.
+ assertTrue(region.contains(2, 2));
+
+ // Test left-bottom corner.
+ assertTrue(region.contains(2, 4));
+
+ // Test right-top corner.
+ assertTrue(region.contains(4, 2));
+
+ // Test right-bottom corner.
+ assertTrue(region.contains(4, 4));
+
+ // Though you set 5, but 5 is not contained by this region.
+ assertFalse(region.contains(5, 5));
+ assertFalse(region.contains(2, 5));
+ assertFalse(region.contains(5, 2));
+
+ // Set a new rectangle.
+ region.set(6, 6, 8, 8);
+ assertFalse(region.contains(3, 3));
+ assertTrue(region.contains(7, 7));
+ }
+
+ @Test
+ public void testEmpty() {
+ assertTrue(region.isEmpty());
+ region = null;
+ region = new Region(1, 2, 3, 4);
+ assertFalse(region.isEmpty());
+ region.setEmpty();
+ assertTrue(region.isEmpty());
+ }
+
+ @Test
+ public void testGetBoundsNull() {
+ assertThrows(NullPointerException.class, () -> region.getBounds(null));
+ }
+
+ @Test
+ public void testGetBounds() {
+ // Normal, return true.
+ Rect rect1 = new Rect(1, 2, 3, 4);
+ region = new Region(rect1);
+ assertTrue(region.getBounds(rect1));
+
+ region.setEmpty();
+ Rect rect2 = new Rect(5, 6, 7, 8);
+ assertFalse(region.getBounds(rect2));
+ }
+
+ @Test
+ public void testOp1() {
+ Rect rect1 = new Rect();
+ Rect rect2 = new Rect(0, 0, 20, 20);
+ Rect rect3 = new Rect(5, 5, 10, 10);
+ Rect rect4 = new Rect(10, 10, 30, 30);
+ Rect rect5 = new Rect(40, 40, 60, 60);
+
+ verifyNullRegionOp1(rect1);
+ verifyDifferenceOp1(rect1, rect2, rect3, rect4, rect5);
+ verifyIntersectOp1(rect1, rect2, rect3, rect4, rect5);
+ verifyUnionOp1(rect1, rect2, rect3, rect4, rect5);
+ verifyXorOp1(rect1, rect2, rect3, rect4, rect5);
+ verifyReverseDifferenceOp1(rect1, rect2, rect3, rect4, rect5);
+ verifyReplaceOp1(rect1, rect2, rect3, rect4, rect5);
+ }
+
+ private void verifyNullRegionOp1(Rect rect1) {
+ // Region without rectangle
+ region = new Region();
+ assertFalse(region.op(rect1, Region.Op.DIFFERENCE));
+ assertFalse(region.op(rect1, Region.Op.INTERSECT));
+ assertFalse(region.op(rect1, Region.Op.UNION));
+ assertFalse(region.op(rect1, Region.Op.XOR));
+ assertFalse(region.op(rect1, Region.Op.REVERSE_DIFFERENCE));
+ assertFalse(region.op(rect1, Region.Op.REPLACE));
+ }
+
+ private void verifyDifferenceOp1(Rect rect1, Rect rect2, Rect rect3, Rect rect4, Rect rect5) {
+ // DIFFERENCE, Region with rectangle
+ // subtract the op region from the first region
+ region = new Region();
+ // subtract null rectangle
+ region.set(rect2);
+ assertTrue(region.op(rect1, Region.Op.DIFFERENCE));
+
+ // 1. subtract rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(rect3, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH1);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT1);
+
+ // 2. subtract rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(11, 11));
+ assertTrue(region.op(rect4, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT2);
+
+ // 3. subtract rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.op(rect5, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyIntersectOp1(Rect rect1, Rect rect2, Rect rect3, Rect rect4, Rect rect5) {
+ // INTERSECT, Region with rectangle
+ // intersect the two regions
+ region = new Region();
+ // intersect null rectangle
+ region.set(rect2);
+ assertFalse(region.op(rect1, Region.Op.INTERSECT));
+
+ // 1. intersect rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.op(rect3, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH1);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT1);
+
+ // 2. intersect rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(9, 9));
+ assertTrue(region.op(rect4, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH2);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT2);
+
+ // 3. intersect rectangle out of this region
+ region.set(rect2);
+ assertFalse(region.op(rect5, Region.Op.INTERSECT));
+ }
+
+ private void verifyUnionOp1(Rect rect1, Rect rect2, Rect rect3, Rect rect4, Rect rect5) {
+ // UNION, Region with rectangle
+ // union (inclusive-or) the two regions
+ region = new Region();
+ region.set(rect2);
+ // union null rectangle
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(rect1, Region.Op.UNION));
+ assertTrue(region.contains(6, 6));
+
+ // 1. union rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(rect3, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH1);
+ verifyPointsOutsideRegion(UNION_WITHOUT1);
+
+ // 2. union rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(rect4, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH2);
+ verifyPointsOutsideRegion(UNION_WITHOUT2);
+
+ // 3. union rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(rect5, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH3);
+ verifyPointsOutsideRegion(UNION_WITHOUT3);
+ }
+
+ private void verifyXorOp1(Rect rect1, Rect rect2, Rect rect3, Rect rect4, Rect rect5) {
+ // XOR, Region with rectangle
+ // exclusive-or the two regions
+ region = new Region();
+ // xor null rectangle
+ region.set(rect2);
+ assertTrue(region.op(rect1, Region.Op.XOR));
+
+ // 1. xor rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(rect3, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH1);
+ verifyPointsOutsideRegion(XOR_WITHOUT1);
+
+ // 2. xor rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(11, 11));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(rect4, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH2);
+ verifyPointsOutsideRegion(XOR_WITHOUT2);
+
+ // 3. xor rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(rect5, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH3);
+ verifyPointsOutsideRegion(XOR_WITHOUT3);
+ }
+
+ private void verifyReverseDifferenceOp1(
+ Rect rect1, Rect rect2, Rect rect3, Rect rect4, Rect rect5) {
+ // REVERSE_DIFFERENCE, Region with rectangle
+ // reverse difference the first region from the op region
+ region = new Region();
+ region.set(rect2);
+ // reverse difference null rectangle
+ assertFalse(region.op(rect1, Region.Op.REVERSE_DIFFERENCE));
+
+ // 1. reverse difference rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertFalse(region.op(rect3, Region.Op.REVERSE_DIFFERENCE));
+
+ // 2. reverse difference rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(11, 11));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(rect4, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT2);
+
+ // 3. reverse difference rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(rect5, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyReplaceOp1(Rect rect1, Rect rect2, Rect rect3, Rect rect4, Rect rect5) {
+ // REPLACE, Region with rectangle
+ // replace the dst region with the op region
+ region = new Region();
+ region.set(rect2);
+ // subtract null rectangle
+ assertFalse(region.op(rect1, Region.Op.REPLACE));
+ // subtract rectangle inside this region
+ region.set(rect2);
+ assertEquals(rect2, region.getBounds());
+ assertTrue(region.op(rect3, Region.Op.REPLACE));
+ assertNotSame(rect2, region.getBounds());
+ assertEquals(rect3, region.getBounds());
+ // subtract rectangle overlap this region
+ region.set(rect2);
+ assertEquals(rect2, region.getBounds());
+ assertTrue(region.op(rect4, Region.Op.REPLACE));
+ assertNotSame(rect2, region.getBounds());
+ assertEquals(rect4, region.getBounds());
+ // subtract rectangle out of this region
+ region.set(rect2);
+ assertEquals(rect2, region.getBounds());
+ assertTrue(region.op(rect5, Region.Op.REPLACE));
+ assertNotSame(rect2, region.getBounds());
+ assertEquals(rect5, region.getBounds());
+ }
+
+ @Test
+ public void testOp2() {
+ Rect rect2 = new Rect(0, 0, 20, 20);
+ Rect rect3 = new Rect(5, 5, 10, 10);
+ Rect rect4 = new Rect(10, 10, 30, 30);
+ Rect rect5 = new Rect(40, 40, 60, 60);
+
+ verifyNullRegionOp2();
+ verifyDifferenceOp2(rect2);
+ verifyIntersectOp2(rect2);
+ verifyUnionOp2(rect2);
+ verifyXorOp2(rect2);
+ verifyReverseDifferenceOp2(rect2);
+ verifyReplaceOp2(rect2, rect3, rect4, rect5);
+ }
+
+ private void verifyNullRegionOp2() {
+ // Region without rectangle
+ region = new Region();
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.DIFFERENCE));
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.INTERSECT));
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.UNION));
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.XOR));
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.REVERSE_DIFFERENCE));
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.REPLACE));
+ }
+
+ private void verifyDifferenceOp2(Rect rect2) {
+ // DIFFERENCE, Region with rectangle
+ // subtract the op region from the first region
+ region = new Region();
+ // subtract null rectangle
+ region.set(rect2);
+ assertTrue(region.op(0, 0, 0, 0, Region.Op.DIFFERENCE));
+
+ // 1. subtract rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(5, 5, 10, 10, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH1);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT1);
+
+ // 2. subtract rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(11, 11));
+ assertTrue(region.op(10, 10, 30, 30, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT2);
+
+ // 3. subtract rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.op(40, 40, 60, 60, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyIntersectOp2(Rect rect2) {
+ // INTERSECT, Region with rectangle
+ // intersect the two regions
+ region = new Region();
+ // intersect null rectangle
+ region.set(rect2);
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.INTERSECT));
+
+ // 1. intersect rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.op(5, 5, 10, 10, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH1);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT1);
+
+ // 2. intersect rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(9, 9));
+ assertTrue(region.op(10, 10, 30, 30, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH2);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT2);
+
+ // 3. intersect rectangle out of this region
+ region.set(rect2);
+ assertFalse(region.op(40, 40, 60, 60, Region.Op.INTERSECT));
+ }
+
+ private void verifyUnionOp2(Rect rect2) {
+ // UNION, Region with rectangle
+ // union (inclusive-or) the two regions
+ region = new Region();
+ region.set(rect2);
+ // union null rectangle
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(0, 0, 0, 0, Region.Op.UNION));
+ assertTrue(region.contains(6, 6));
+
+ // 1. union rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(5, 5, 10, 10, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH1);
+ verifyPointsOutsideRegion(UNION_WITHOUT1);
+
+ // 2. union rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(10, 10, 30, 30, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH2);
+ verifyPointsOutsideRegion(UNION_WITHOUT2);
+
+ // 3. union rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(40, 40, 60, 60, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH3);
+ verifyPointsOutsideRegion(UNION_WITHOUT3);
+ }
+
+ private void verifyXorOp2(Rect rect2) {
+ // XOR, Region with rectangle
+ // exclusive-or the two regions
+ region = new Region();
+ region.set(rect2);
+ // xor null rectangle
+ assertTrue(region.op(0, 0, 0, 0, Region.Op.XOR));
+
+ // 1. xor rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(5, 5, 10, 10, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH1);
+ verifyPointsOutsideRegion(XOR_WITHOUT1);
+
+ // 2. xor rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(11, 11));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(10, 10, 30, 30, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH2);
+ verifyPointsOutsideRegion(XOR_WITHOUT2);
+
+ // 3. xor rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(40, 40, 60, 60, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH3);
+ verifyPointsOutsideRegion(XOR_WITHOUT3);
+ }
+
+ private void verifyReverseDifferenceOp2(Rect rect2) {
+ // REVERSE_DIFFERENCE, Region with rectangle
+ // reverse difference the first region from the op region
+ region = new Region();
+ region.set(rect2);
+ // reverse difference null rectangle
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.REVERSE_DIFFERENCE));
+ // reverse difference rectangle inside this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertFalse(region.op(5, 5, 10, 10, Region.Op.REVERSE_DIFFERENCE));
+ // reverse difference rectangle overlap this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(11, 11));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(10, 10, 30, 30, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT2);
+ // reverse difference rectangle out of this region
+ region.set(rect2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(40, 40, 60, 60, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyReplaceOp2(Rect rect2, Rect rect3, Rect rect4, Rect rect5) {
+ // REPLACE, Region w1ith rectangle
+ // replace the dst region with the op region
+ region = new Region();
+ region.set(rect2);
+ // subtract null rectangle
+ assertFalse(region.op(0, 0, 0, 0, Region.Op.REPLACE));
+ // subtract rectangle inside this region
+ region.set(rect2);
+ assertEquals(rect2, region.getBounds());
+ assertTrue(region.op(5, 5, 10, 10, Region.Op.REPLACE));
+ assertNotSame(rect2, region.getBounds());
+ assertEquals(rect3, region.getBounds());
+ // subtract rectangle overlap this region
+ region.set(rect2);
+ assertEquals(rect2, region.getBounds());
+ assertTrue(region.op(10, 10, 30, 30, Region.Op.REPLACE));
+ assertNotSame(rect2, region.getBounds());
+ assertEquals(rect4, region.getBounds());
+ // subtract rectangle out of this region
+ region.set(rect2);
+ assertEquals(rect2, region.getBounds());
+ assertTrue(region.op(40, 40, 60, 60, Region.Op.REPLACE));
+ assertNotSame(rect2, region.getBounds());
+ assertEquals(rect5, region.getBounds());
+ }
+
+ @Test
+ public void testOp3() {
+ Region region1 = new Region();
+ Region region2 = new Region(0, 0, 20, 20);
+ Region region3 = new Region(5, 5, 10, 10);
+ Region region4 = new Region(10, 10, 30, 30);
+ Region region5 = new Region(40, 40, 60, 60);
+
+ verifyNullRegionOp3(region1);
+ verifyDifferenceOp3(region1, region2, region3, region4, region5);
+ verifyIntersectOp3(region1, region2, region3, region4, region5);
+ verifyUnionOp3(region1, region2, region3, region4, region5);
+ verifyXorOp3(region1, region2, region3, region4, region5);
+ verifyReverseDifferenceOp3(region1, region2, region3, region4, region5);
+ verifyReplaceOp3(region1, region2, region3, region4, region5);
+ }
+
+ private void verifyNullRegionOp3(Region region1) {
+ // Region without rectangle
+ region = new Region();
+ assertFalse(region.op(region1, Region.Op.DIFFERENCE));
+ assertFalse(region.op(region1, Region.Op.INTERSECT));
+ assertFalse(region.op(region1, Region.Op.UNION));
+ assertFalse(region.op(region1, Region.Op.XOR));
+ assertFalse(region.op(region1, Region.Op.REVERSE_DIFFERENCE));
+ assertFalse(region.op(region1, Region.Op.REPLACE));
+ }
+
+ private void verifyDifferenceOp3(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // DIFFERENCE, Region with rectangle
+ // subtract the op region from the first region
+ region = new Region();
+ // subtract null rectangle
+ region.set(region2);
+ assertTrue(region.op(region1, Region.Op.DIFFERENCE));
+
+ // 1. subtract rectangle inside this region
+ region.set(region2);
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(region3, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH1);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT1);
+
+ // 2. subtract rectangle overlap this region
+ region.set(region2);
+ assertTrue(region.contains(11, 11));
+ assertTrue(region.op(region4, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT2);
+
+ // 3. subtract rectangle out of this region
+ region.set(region2);
+ assertTrue(region.op(region5, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyIntersectOp3(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // INTERSECT, Region with rectangle
+ // intersect the two regions
+ region = new Region();
+ region.set(region2);
+ // intersect null rectangle
+ assertFalse(region.op(region1, Region.Op.INTERSECT));
+
+ // 1. intersect rectangle inside this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.op(region3, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH1);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT1);
+
+ // 2. intersect rectangle overlap this region
+ region.set(region2);
+ assertTrue(region.contains(9, 9));
+ assertTrue(region.op(region4, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH2);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT2);
+
+ // 3. intersect rectangle out of this region
+ region.set(region2);
+ assertFalse(region.op(region5, Region.Op.INTERSECT));
+ }
+
+ private void verifyUnionOp3(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // UNION, Region with rectangle
+ // union (inclusive-or) the two regions
+ region = new Region();
+ // union null rectangle
+ region.set(region2);
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(region1, Region.Op.UNION));
+ assertTrue(region.contains(6, 6));
+
+ // 1. union rectangle inside this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(region3, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH1);
+ verifyPointsOutsideRegion(UNION_WITHOUT1);
+
+ // 2. union rectangle overlap this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(region4, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH2);
+ verifyPointsOutsideRegion(UNION_WITHOUT2);
+
+ // 3. union rectangle out of this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(region5, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH3);
+ verifyPointsOutsideRegion(UNION_WITHOUT3);
+ }
+
+ private void verifyXorOp3(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // XOR, Region with rectangle
+ // exclusive-or the two regions
+ region = new Region();
+ // xor null rectangle
+ region.set(region2);
+ assertTrue(region.op(region1, Region.Op.XOR));
+
+ // 1. xor rectangle inside this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertTrue(region.op(region3, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH1);
+ verifyPointsOutsideRegion(XOR_WITHOUT1);
+
+ // 2. xor rectangle overlap this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(11, 11));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(region4, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH2);
+ verifyPointsOutsideRegion(XOR_WITHOUT2);
+
+ // 3. xor rectangle out of this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(region5, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH3);
+ verifyPointsOutsideRegion(XOR_WITHOUT3);
+ }
+
+ private void verifyReverseDifferenceOp3(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // REVERSE_DIFFERENCE, Region with rectangle
+ // reverse difference the first region from the op region
+ region = new Region();
+ // reverse difference null rectangle
+ region.set(region2);
+ assertFalse(region.op(region1, Region.Op.REVERSE_DIFFERENCE));
+
+ // 1. reverse difference rectangle inside this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(6, 6));
+ assertFalse(region.op(region3, Region.Op.REVERSE_DIFFERENCE));
+
+ // 2. reverse difference rectangle overlap this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertTrue(region.contains(11, 11));
+ assertFalse(region.contains(21, 21));
+ assertTrue(region.op(region4, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT2);
+
+ // 3. reverse difference rectangle out of this region
+ region.set(region2);
+ assertTrue(region.contains(2, 2));
+ assertFalse(region.contains(41, 41));
+ assertTrue(region.op(region5, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyReplaceOp3(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // REPLACE, Region with rectangle
+ // replace the dst region with the op region
+ region = new Region();
+ region.set(region2);
+ // subtract null rectangle
+ assertFalse(region.op(region1, Region.Op.REPLACE));
+ // subtract rectangle inside this region
+ region.set(region2);
+ assertEquals(region2.getBounds(), region.getBounds());
+ assertTrue(region.op(region3, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region3.getBounds(), region.getBounds());
+ // subtract rectangle overlap this region
+ region.set(region2);
+ assertEquals(region2.getBounds(), region.getBounds());
+ assertTrue(region.op(region4, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region4.getBounds(), region.getBounds());
+ // subtract rectangle out of this region
+ region.set(region2);
+ assertEquals(region2.getBounds(), region.getBounds());
+ assertTrue(region.op(region5, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region5.getBounds(), region.getBounds());
+ }
+
+ @Test
+ public void testOp4() {
+ Rect rect1 = new Rect();
+ Rect rect2 = new Rect(0, 0, 20, 20);
+
+ Region region1 = new Region();
+ Region region2 = new Region(0, 0, 20, 20);
+ Region region3 = new Region(5, 5, 10, 10);
+ Region region4 = new Region(10, 10, 30, 30);
+ Region region5 = new Region(40, 40, 60, 60);
+
+ verifyNullRegionOp4(rect1, region1);
+ verifyDifferenceOp4(rect1, rect2, region1, region3, region4, region5);
+ verifyIntersectOp4(rect1, rect2, region1, region3, region4, region5);
+ verifyUnionOp4(rect1, rect2, region1, region3, region4, region5);
+ verifyXorOp4(rect1, rect2, region1, region3, region4, region5);
+ verifyReverseDifferenceOp4(rect1, rect2, region1, region3, region4, region5);
+ verifyReplaceOp4(rect1, rect2, region1, region2, region3, region4, region5);
+ }
+
+ private void verifyNullRegionOp4(Rect rect1, Region region1) {
+ // Region without rectangle
+ region = new Region();
+ assertFalse(region.op(rect1, region1, Region.Op.DIFFERENCE));
+ assertFalse(region.op(rect1, region1, Region.Op.INTERSECT));
+ assertFalse(region.op(rect1, region1, Region.Op.UNION));
+
+ assertFalse(region.op(rect1, region1, Region.Op.XOR));
+ assertFalse(region.op(rect1, region1, Region.Op.REVERSE_DIFFERENCE));
+ assertFalse(region.op(rect1, region1, Region.Op.REPLACE));
+ }
+
+ private void verifyDifferenceOp4(
+ Rect rect1, Rect rect2, Region region1, Region region3, Region region4, Region region5) {
+ // DIFFERENCE, Region with rectangle
+ // subtract the op region from the first region
+ region = new Region();
+ // subtract null rectangle
+ assertTrue(region.op(rect2, region1, Region.Op.DIFFERENCE));
+
+ // 1. subtract rectangle inside this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region3, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH1);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT1);
+
+ // 2. subtract rectangle overlap this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region4, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT2);
+
+ // 3. subtract rectangle out of this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region5, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyIntersectOp4(
+ Rect rect1, Rect rect2, Region region1, Region region3, Region region4, Region region5) {
+ // INTERSECT, Region with rectangle
+ // intersect the two regions
+ region = new Region();
+ // intersect null rectangle
+ region.set(rect1);
+ assertFalse(region.op(rect2, region1, Region.Op.INTERSECT));
+
+ // 1. intersect rectangle inside this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region3, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH1);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT1);
+
+ // 2. intersect rectangle overlap this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region4, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH2);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT2);
+
+ // 3. intersect rectangle out of this region
+ region.set(rect1);
+ assertFalse(region.op(rect2, region5, Region.Op.INTERSECT));
+ }
+
+ private void verifyUnionOp4(
+ Rect rect1, Rect rect2, Region region1, Region region3, Region region4, Region region5) {
+ // UNION, Region with rectangle
+ // union (inclusive-or) the two regions
+ region = new Region();
+ // union null rectangle
+ region.set(rect1);
+ assertTrue(region.op(rect2, region1, Region.Op.UNION));
+ assertTrue(region.contains(6, 6));
+
+ // 1. union rectangle inside this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region3, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH1);
+ verifyPointsOutsideRegion(UNION_WITHOUT1);
+
+ // 2. union rectangle overlap this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region4, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH2);
+ verifyPointsOutsideRegion(UNION_WITHOUT2);
+
+ // 3. union rectangle out of this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region5, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH3);
+ verifyPointsOutsideRegion(UNION_WITHOUT3);
+ }
+
+ private void verifyXorOp4(
+ Rect rect1, Rect rect2, Region region1, Region region3, Region region4, Region region5) {
+ // XOR, Region with rectangle
+ // exclusive-or the two regions
+ region = new Region();
+ // xor null rectangle
+ region.set(rect1);
+ assertTrue(region.op(rect2, region1, Region.Op.XOR));
+
+ // 1. xor rectangle inside this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region3, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH1);
+ verifyPointsOutsideRegion(XOR_WITHOUT1);
+
+ // 2. xor rectangle overlap this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region4, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH2);
+ verifyPointsOutsideRegion(XOR_WITHOUT2);
+
+ // 3. xor rectangle out of this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region5, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH3);
+ verifyPointsOutsideRegion(XOR_WITHOUT3);
+ }
+
+ private void verifyReverseDifferenceOp4(
+ Rect rect1, Rect rect2, Region region1, Region region3, Region region4, Region region5) {
+ // REVERSE_DIFFERENCE, Region with rectangle
+ // reverse difference the first region from the op region
+ region = new Region();
+ // reverse difference null rectangle
+ region.set(rect1);
+ assertFalse(region.op(rect2, region1, Region.Op.REVERSE_DIFFERENCE));
+
+ // 1. reverse difference rectangle inside this region
+ region.set(rect1);
+ assertFalse(region.op(rect2, region3, Region.Op.REVERSE_DIFFERENCE));
+
+ // 2. reverse difference rectangle overlap this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region4, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT2);
+
+ // 3. reverse difference rectangle out of this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region5, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyReplaceOp4(
+ Rect rect1,
+ Rect rect2,
+ Region region1,
+ Region region2,
+ Region region3,
+ Region region4,
+ Region region5) {
+ // REPLACE, Region with rectangle
+ // replace the dst region with the op region
+ region = new Region();
+ // subtract null rectangle
+ region.set(rect1);
+ assertFalse(region.op(rect2, region1, Region.Op.REPLACE));
+ // subtract rectangle inside this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region3, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region3.getBounds(), region.getBounds());
+ // subtract rectangle overlap this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region4, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region4.getBounds(), region.getBounds());
+ // subtract rectangle out of this region
+ region.set(rect1);
+ assertTrue(region.op(rect2, region5, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region5.getBounds(), region.getBounds());
+ }
+
+ @Test
+ public void testOp5() {
+ Region region1 = new Region();
+ Region region2 = new Region(0, 0, 20, 20);
+ Region region3 = new Region(5, 5, 10, 10);
+ Region region4 = new Region(10, 10, 30, 30);
+ Region region5 = new Region(40, 40, 60, 60);
+
+ verifyNullRegionOp5(region1);
+ verifyDifferenceOp5(region1, region2, region3, region4, region5);
+ verifyIntersectOp5(region1, region2, region3, region4, region5);
+ verifyUnionOp5(region1, region2, region3, region4, region5);
+ verifyXorOp5(region1, region2, region3, region4, region5);
+ verifyReverseDifferenceOp5(region1, region2, region3, region4, region5);
+ verifyReplaceOp5(region1, region2, region3, region4, region5);
+ }
+
+ private void verifyNullRegionOp5(Region region1) {
+ // Region without rectangle
+ region = new Region();
+ assertFalse(region.op(region, region1, Region.Op.DIFFERENCE));
+ assertFalse(region.op(region, region1, Region.Op.INTERSECT));
+ assertFalse(region.op(region, region1, Region.Op.UNION));
+ assertFalse(region.op(region, region1, Region.Op.XOR));
+ assertFalse(region.op(region, region1, Region.Op.REVERSE_DIFFERENCE));
+ assertFalse(region.op(region, region1, Region.Op.REPLACE));
+ }
+
+ private void verifyDifferenceOp5(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // DIFFERENCE, Region with rectangle
+ // subtract the op region from the first region
+ region = new Region();
+ // subtract null rectangle
+ region.set(region1);
+ assertTrue(region.op(region2, region1, Region.Op.DIFFERENCE));
+
+ // 1. subtract rectangle inside this region
+ region.set(region1);
+ assertTrue(region.op(region2, region3, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH1);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT1);
+
+ // 2. subtract rectangle overlap this region
+ region.set(region1);
+ assertTrue(region.op(region2, region4, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT2);
+
+ // 3. subtract rectangle out of this region
+ region.set(region1);
+ assertTrue(region.op(region2, region5, Region.Op.DIFFERENCE));
+ verifyPointsInsideRegion(DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyIntersectOp5(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // INTERSECT, Region with rectangle
+ // intersect the two regions
+ region = new Region();
+ // intersect null rectangle
+ region.set(region1);
+ assertFalse(region.op(region2, region1, Region.Op.INTERSECT));
+
+ // 1. intersect rectangle inside this region
+ region.set(region1);
+ assertTrue(region.op(region2, region3, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH1);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT1);
+
+ // 2. intersect rectangle overlap this region
+ region.set(region1);
+ assertTrue(region.op(region2, region4, Region.Op.INTERSECT));
+ verifyPointsInsideRegion(INTERSECT_WITH2);
+ verifyPointsOutsideRegion(INTERSECT_WITHOUT2);
+
+ // 3. intersect rectangle out of this region
+ region.set(region1);
+ assertFalse(region.op(region2, region5, Region.Op.INTERSECT));
+ }
+
+ private void verifyUnionOp5(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // UNION, Region with rectangle
+ // union (inclusive-or) the two regions
+ region = new Region();
+ // union null rectangle
+ region.set(region1);
+ assertTrue(region.op(region2, region1, Region.Op.UNION));
+ assertTrue(region.contains(6, 6));
+
+ // 1. union rectangle inside this region
+ region.set(region1);
+ assertTrue(region.op(region2, region3, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH1);
+ verifyPointsOutsideRegion(UNION_WITHOUT1);
+
+ // 2. union rectangle overlap this region
+ region.set(region1);
+ assertTrue(region.op(region2, region4, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH2);
+ verifyPointsOutsideRegion(UNION_WITHOUT2);
+
+ // 3. union rectangle out of this region
+ region.set(region1);
+ assertTrue(region.op(region2, region5, Region.Op.UNION));
+ verifyPointsInsideRegion(UNION_WITH3);
+ verifyPointsOutsideRegion(UNION_WITHOUT3);
+ }
+
+ private void verifyXorOp5(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // XOR, Region with rectangle
+ // exclusive-or the two regions
+ region = new Region();
+ // xor null rectangle
+ region.set(region1);
+ assertTrue(region.op(region2, region1, Region.Op.XOR));
+
+ // 1. xor rectangle inside this region
+ region.set(region1);
+ assertTrue(region.op(region2, region3, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH1);
+ verifyPointsOutsideRegion(XOR_WITHOUT1);
+
+ // 2. xor rectangle overlap this region
+ region.set(region1);
+ assertTrue(region.op(region2, region4, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH2);
+ verifyPointsOutsideRegion(XOR_WITHOUT2);
+
+ // 3. xor rectangle out of this region
+ region.set(region1);
+ assertTrue(region.op(region2, region5, Region.Op.XOR));
+ verifyPointsInsideRegion(XOR_WITH3);
+ verifyPointsOutsideRegion(XOR_WITHOUT3);
+ }
+
+ private void verifyReverseDifferenceOp5(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // REVERSE_DIFFERENCE, Region with rectangle
+ // reverse difference the first region from the op region
+ region = new Region();
+ // reverse difference null rectangle
+ region.set(region1);
+ assertFalse(region.op(region2, region1, Region.Op.REVERSE_DIFFERENCE));
+
+ // 1. reverse difference rectangle inside this region
+ region.set(region1);
+ assertFalse(region.op(region2, region3, Region.Op.REVERSE_DIFFERENCE));
+
+ // 2. reverse difference rectangle overlap this region
+ region.set(region1);
+ assertTrue(region.op(region2, region4, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH2);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT2);
+
+ // 3. reverse difference rectangle out of this region
+ region.set(region1);
+ assertTrue(region.op(region2, region5, Region.Op.REVERSE_DIFFERENCE));
+ verifyPointsInsideRegion(REVERSE_DIFFERENCE_WITH3);
+ verifyPointsOutsideRegion(REVERSE_DIFFERENCE_WITHOUT3);
+ }
+
+ private void verifyReplaceOp5(
+ Region region1, Region region2, Region region3, Region region4, Region region5) {
+ // REPLACE, Region with rectangle
+ // replace the dst region with the op region
+ region = new Region();
+ // subtract null rectangle
+ region.set(region1);
+ assertFalse(region.op(region2, region1, Region.Op.REPLACE));
+ // subtract rectangle inside this region
+ region.set(region1);
+ assertTrue(region.op(region2, region3, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region3.getBounds(), region.getBounds());
+ // subtract rectangle overlap this region
+ region.set(region1);
+ assertTrue(region.op(region2, region4, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region4.getBounds(), region.getBounds());
+ // subtract rectangle out of this region
+ region.set(region1);
+ assertTrue(region.op(region2, region5, Region.Op.REPLACE));
+ assertNotSame(region2.getBounds(), region.getBounds());
+ assertEquals(region5.getBounds(), region.getBounds());
+ }
+
+ @Test
+ public void testGetBoundaryPath1() {
+ assertTrue(region.getBoundaryPath().isEmpty());
+
+ // Both clip and path are non-null.
+ Region clip = new Region(0, 0, 10, 10);
+ Path path = new Path();
+ path.addRect(0, 0, 10, 10, Path.Direction.CW);
+ assertTrue(region.setPath(path, clip));
+ assertFalse(region.getBoundaryPath().isEmpty());
+ }
+
+ @Test
+ public void testGetBoundaryPath2() {
+ Path path = new Path();
+ assertFalse(region.getBoundaryPath(path));
+
+ // path is null
+ region = new Region(0, 0, 10, 10);
+ path = new Path();
+ assertTrue(region.getBoundaryPath(path));
+
+ // region is null
+ region = new Region();
+ path = new Path();
+ path.addRect(0, 0, 10, 10, Path.Direction.CW);
+ assertFalse(region.getBoundaryPath(path));
+
+ // both path and region are non-null
+ region = new Region(0, 0, 10, 10);
+ path = new Path();
+ path.addRect(0, 0, 5, 5, Path.Direction.CW);
+ assertTrue(region.getBoundaryPath(path));
+ }
+
+ @Test
+ public void testSetPath() {
+ // Both clip and path are null.
+ Region clip = new Region();
+ Path path = new Path();
+ assertFalse(region.setPath(path, clip));
+
+ // Only path is null.
+ path = new Path();
+ clip = new Region(0, 0, 10, 10);
+ assertFalse(region.setPath(path, clip));
+
+ // Only clip is null.
+ clip = new Region();
+ path = new Path();
+ path.addRect(0, 0, 10, 10, Path.Direction.CW);
+ assertFalse(region.setPath(path, clip));
+
+ // Both clip and path are non-null.
+ path = new Path();
+ clip = new Region(0, 0, 10, 10);
+ path.addRect(0, 0, 10, 10, Path.Direction.CW);
+ assertTrue(region.setPath(path, clip));
+
+ // Both clip and path are non-null.
+ path = new Path();
+ clip = new Region(0, 0, 5, 5);
+ path.addRect(0, 0, 10, 10, Path.Direction.CW);
+ assertTrue(region.setPath(path, clip));
+ Rect expected = new Rect(0, 0, 5, 5);
+ Rect unexpected = new Rect(0, 0, 10, 10);
+ Rect actual = region.getBounds();
+ assertEquals(expected.right, actual.right);
+ assertNotSame(unexpected.right, actual.right);
+
+ // Both clip and path are non-null.
+ path = new Path();
+ clip = new Region(0, 0, 10, 10);
+ path.addRect(0, 0, 5, 5, Path.Direction.CW);
+ assertTrue(region.setPath(path, clip));
+ expected = new Rect(0, 0, 5, 5);
+ unexpected = new Rect(0, 0, 10, 10);
+ actual = region.getBounds();
+ assertEquals(expected.right, actual.right);
+ assertNotSame(unexpected.right, actual.right);
+ }
+
+ @Test
+ public void testTranslate1() {
+ Rect rect1 = new Rect(0, 0, 20, 20);
+ Rect rect2 = new Rect(10, 10, 30, 30);
+ region = new Region(0, 0, 20, 20);
+ region.translate(10, 10);
+ assertNotSame(rect1, region.getBounds());
+ assertEquals(rect2, region.getBounds());
+ }
+
+ @Test
+ public void testTranslate2() {
+ Region dst = new Region();
+ Rect rect1 = new Rect(0, 0, 20, 20);
+ Rect rect2 = new Rect(10, 10, 30, 30);
+ region = new Region(0, 0, 20, 20);
+ region.translate(10, 10, dst);
+ assertEquals(rect1, region.getBounds());
+ assertNotSame(rect2, region.getBounds());
+ assertNotSame(rect1, dst.getBounds());
+ assertEquals(rect2, dst.getBounds());
+ }
+
+ @Test
+ public void testDescribeContents() {
+ int actual = region.describeContents();
+ assertEquals(0, actual);
+ }
+
+ @Test
+ public void testQuickReject1() {
+ Rect oriRect = new Rect(0, 0, 20, 20);
+ Rect rect1 = new Rect();
+ Rect rect2 = new Rect(40, 40, 60, 60);
+ Rect rect3 = new Rect(0, 0, 10, 10);
+ Rect rect4 = new Rect(10, 10, 30, 30);
+
+ // Return true if the region is empty
+ assertTrue(region.quickReject(rect1));
+ region.set(oriRect);
+ assertTrue(region.quickReject(rect2));
+ region.set(oriRect);
+ assertFalse(region.quickReject(rect3));
+ region.set(oriRect);
+ assertFalse(region.quickReject(rect4));
+ }
+
+ @Test
+ public void testQuickReject2() {
+ // Return true if the region is empty
+ assertTrue(region.quickReject(0, 0, 0, 0));
+ region.set(0, 0, 20, 20);
+ assertTrue(region.quickReject(40, 40, 60, 60));
+ region.set(0, 0, 20, 20);
+ assertFalse(region.quickReject(0, 0, 10, 10));
+ region.set(0, 0, 20, 20);
+ assertFalse(region.quickReject(10, 10, 30, 30));
+ }
+
+ @Test
+ public void testQuickReject3() {
+ Region oriRegion = new Region(0, 0, 20, 20);
+ Region region1 = new Region();
+ Region region2 = new Region(40, 40, 60, 60);
+ Region region3 = new Region(0, 0, 10, 10);
+ Region region4 = new Region(10, 10, 30, 30);
+
+ // Return true if the region is empty
+ assertTrue(region.quickReject(region1));
+ region.set(oriRegion);
+ assertTrue(region.quickReject(region2));
+ region.set(oriRegion);
+ assertFalse(region.quickReject(region3));
+ region.set(oriRegion);
+ assertFalse(region.quickReject(region4));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRenderNodeAnimatorTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRenderNodeAnimatorTest.java
new file mode 100644
index 000000000..938f0c278
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRenderNodeAnimatorTest.java
@@ -0,0 +1,25 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.CanvasProperty;
+import android.graphics.Paint;
+import android.view.RenderNodeAnimator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowNativeRenderNodeAnimatorTest {
+ @Test
+ public void testConstruction() {
+ Paint paint = new Paint();
+ CanvasProperty<Paint> prop = CanvasProperty.createPaint(paint);
+ RenderNodeAnimator opacityAnim =
+ new RenderNodeAnimator(prop, RenderNodeAnimator.PAINT_ALPHA, 0);
+ opacityAnim.setStartDelay(100L);
+ opacityAnim.setStartValue(0f);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRenderNodeTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRenderNodeTest.java
new file mode 100644
index 000000000..04698186b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRenderNodeTest.java
@@ -0,0 +1,277 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.view.View;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
+import org.robolectric.util.reflector.WithType;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = Q)
+public class ShadowNativeRenderNodeTest {
+ @Test
+ public void testDefaults() {
+ final RenderNode renderNode = new RenderNode(null);
+ assertEquals(0, renderNode.getLeft());
+ assertEquals(0, renderNode.getRight());
+ assertEquals(0, renderNode.getTop());
+ assertEquals(0, renderNode.getBottom());
+ assertEquals(0, renderNode.getWidth());
+ assertEquals(0, renderNode.getHeight());
+
+ assertEquals(0, renderNode.getTranslationX(), 0.01f);
+ assertEquals(0, renderNode.getTranslationY(), 0.01f);
+ assertEquals(0, renderNode.getTranslationZ(), 0.01f);
+ assertEquals(0, renderNode.getElevation(), 0.01f);
+
+ assertEquals(0, renderNode.getRotationX(), 0.01f);
+ assertEquals(0, renderNode.getRotationY(), 0.01f);
+ assertEquals(0, renderNode.getRotationZ(), 0.01f);
+
+ assertEquals(1, renderNode.getScaleX(), 0.01f);
+ assertEquals(1, renderNode.getScaleY(), 0.01f);
+
+ assertEquals(1, renderNode.getAlpha(), 0.01f);
+
+ assertEquals(0, renderNode.getPivotX(), 0.01f);
+ assertEquals(0, renderNode.getPivotY(), 0.01f);
+
+ assertEquals(Color.BLACK, renderNode.getAmbientShadowColor());
+ assertEquals(Color.BLACK, renderNode.getSpotShadowColor());
+
+ assertEquals(8, renderNode.getCameraDistance(), 0.01f);
+
+ assertTrue(renderNode.isForceDarkAllowed());
+ assertTrue(renderNode.hasIdentityMatrix());
+ assertTrue(renderNode.getClipToBounds());
+ assertFalse(renderNode.getClipToOutline());
+ assertFalse(renderNode.isPivotExplicitlySet());
+ assertFalse(renderNode.hasDisplayList());
+ assertFalse(renderNode.hasOverlappingRendering());
+ assertFalse(renderNode.hasShadow());
+ assertFalse(renderNode.getUseCompositingLayer());
+ }
+
+ @Test
+ @Config(sdk = 31)
+ public void testBasicDraw() {
+ final Rect rect = new Rect(10, 10, 80, 80);
+
+ final RenderNode renderNode = new RenderNode("Blue rect");
+ assertTrue(renderNode.setPosition(rect.left, rect.top, rect.right, rect.bottom));
+ assertEquals(rect.left, renderNode.getLeft());
+ assertEquals(rect.top, renderNode.getTop());
+ assertEquals(rect.right, renderNode.getRight());
+ assertEquals(rect.bottom, renderNode.getBottom());
+ renderNode.setClipToBounds(true);
+
+ {
+ Canvas canvas = renderNode.beginRecording();
+ assertEquals(rect.width(), canvas.getWidth());
+ assertEquals(rect.height(), canvas.getHeight());
+ assertTrue(canvas.isHardwareAccelerated());
+ canvas.drawColor(Color.BLUE);
+ renderNode.endRecording();
+ }
+
+ assertTrue(renderNode.hasDisplayList());
+ assertTrue(renderNode.hasIdentityMatrix());
+ }
+
+ @Test
+ public void testTranslationGetSet() {
+ final RenderNode renderNode = new RenderNode("translation");
+
+ assertTrue(renderNode.hasIdentityMatrix());
+
+ assertFalse(renderNode.setTranslationX(0.0f));
+ assertFalse(renderNode.setTranslationY(0.0f));
+ assertFalse(renderNode.setTranslationZ(0.0f));
+
+ assertTrue(renderNode.hasIdentityMatrix());
+
+ assertTrue(renderNode.setTranslationX(1.0f));
+ assertEquals(1.0f, renderNode.getTranslationX(), 0.0f);
+ assertTrue(renderNode.setTranslationY(1.0f));
+ assertEquals(1.0f, renderNode.getTranslationY(), 0.0f);
+ assertTrue(renderNode.setTranslationZ(1.0f));
+ assertEquals(1.0f, renderNode.getTranslationZ(), 0.0f);
+
+ assertFalse(renderNode.hasIdentityMatrix());
+
+ assertTrue(renderNode.setTranslationX(0.0f));
+ assertTrue(renderNode.setTranslationY(0.0f));
+ assertTrue(renderNode.setTranslationZ(0.0f));
+
+ assertTrue(renderNode.hasIdentityMatrix());
+ }
+
+ @Test
+ public void testAlphaGetSet() {
+ final RenderNode renderNode = new RenderNode("alpha");
+
+ assertFalse(renderNode.setAlpha(1.0f));
+ assertTrue(renderNode.setAlpha(.5f));
+ assertEquals(.5f, renderNode.getAlpha(), 0.0001f);
+ assertTrue(renderNode.setAlpha(1.0f));
+ }
+
+ @Test
+ public void testRotationGetSet() {
+ final RenderNode renderNode = new RenderNode("rotation");
+
+ assertFalse(renderNode.setRotationX(0.0f));
+ assertFalse(renderNode.setRotationY(0.0f));
+ assertFalse(renderNode.setRotationZ(0.0f));
+ assertTrue(renderNode.hasIdentityMatrix());
+
+ assertTrue(renderNode.setRotationX(1.0f));
+ assertEquals(1.0f, renderNode.getRotationX(), 0.0f);
+ assertTrue(renderNode.setRotationY(1.0f));
+ assertEquals(1.0f, renderNode.getRotationY(), 0.0f);
+ assertTrue(renderNode.setRotationZ(1.0f));
+ assertEquals(1.0f, renderNode.getRotationZ(), 0.0f);
+ assertFalse(renderNode.hasIdentityMatrix());
+
+ assertTrue(renderNode.setRotationX(0.0f));
+ assertTrue(renderNode.setRotationY(0.0f));
+ assertTrue(renderNode.setRotationZ(0.0f));
+ assertTrue(renderNode.hasIdentityMatrix());
+ }
+
+ @Test
+ public void testScaleGetSet() {
+ final RenderNode renderNode = new RenderNode("scale");
+
+ assertFalse(renderNode.setScaleX(1.0f));
+ assertFalse(renderNode.setScaleY(1.0f));
+
+ assertTrue(renderNode.setScaleX(2.0f));
+ assertEquals(2.0f, renderNode.getScaleX(), 0.0f);
+ assertTrue(renderNode.setScaleY(2.0f));
+ assertEquals(2.0f, renderNode.getScaleY(), 0.0f);
+
+ assertTrue(renderNode.setScaleX(1.0f));
+ assertTrue(renderNode.setScaleY(1.0f));
+ }
+
+ @Test
+ public void testStartEndRecordingEmpty() {
+ final RenderNode renderNode = new RenderNode(null);
+ assertEquals(0, renderNode.getWidth());
+ assertEquals(0, renderNode.getHeight());
+ RecordingCanvas canvas = renderNode.beginRecording();
+ assertTrue(canvas.isHardwareAccelerated());
+ assertEquals(0, canvas.getWidth());
+ assertEquals(0, canvas.getHeight());
+ renderNode.endRecording();
+ }
+
+ @Test
+ public void testStartEndRecordingWithBounds() {
+ final RenderNode renderNode = new RenderNode(null);
+ renderNode.setPosition(10, 20, 30, 50);
+ assertEquals(20, renderNode.getWidth());
+ assertEquals(30, renderNode.getHeight());
+ RecordingCanvas canvas = renderNode.beginRecording();
+ assertTrue(canvas.isHardwareAccelerated());
+ assertEquals(20, canvas.getWidth());
+ assertEquals(30, canvas.getHeight());
+ renderNode.endRecording();
+ }
+
+ @Test
+ public void testStartEndRecordingEmptyWithSize() {
+ final RenderNode renderNode = new RenderNode(null);
+ assertEquals(0, renderNode.getWidth());
+ assertEquals(0, renderNode.getHeight());
+ RecordingCanvas canvas = renderNode.beginRecording(5, 10);
+ assertTrue(canvas.isHardwareAccelerated());
+ assertEquals(5, canvas.getWidth());
+ assertEquals(10, canvas.getHeight());
+ renderNode.endRecording();
+ }
+
+ @Test
+ public void testStartEndRecordingWithBoundsWithSize() {
+ final RenderNode renderNode = new RenderNode(null);
+ renderNode.setPosition(10, 20, 30, 50);
+ assertEquals(20, renderNode.getWidth());
+ assertEquals(30, renderNode.getHeight());
+ RecordingCanvas canvas = renderNode.beginRecording(5, 10);
+ assertTrue(canvas.isHardwareAccelerated());
+ assertEquals(5, canvas.getWidth());
+ assertEquals(10, canvas.getHeight());
+ renderNode.endRecording();
+ }
+
+ @Test
+ public void testGetUniqueId() {
+ final RenderNode r1 = new RenderNode(null);
+ final RenderNode r2 = new RenderNode(null);
+ assertNotEquals(r1.getUniqueId(), r2.getUniqueId());
+ final Set<Long> usedIds = new HashSet<>();
+ assertTrue(usedIds.add(r1.getUniqueId()));
+ assertTrue(usedIds.add(r2.getUniqueId()));
+ for (int i = 0; i < 100; i++) {
+ assertTrue(usedIds.add(new RenderNode(null).getUniqueId()));
+ }
+ }
+
+ @Test
+ public void testInvalidCameraDistance() {
+ final RenderNode renderNode = new RenderNode(null);
+ assertThrows(IllegalArgumentException.class, () -> renderNode.setCameraDistance(-1f));
+ }
+
+ @Test
+ public void testCameraDistanceSetGet() {
+ final RenderNode renderNode = new RenderNode(null);
+ renderNode.setCameraDistance(100f);
+ assertEquals(100f, renderNode.getCameraDistance(), 0.0f);
+ }
+
+ @Test
+ @Config(minSdk = O, maxSdk = P)
+ public void testIsValid() throws Exception {
+ Object renderNode = reflector(RenderNodeOpReflector.class).create("name", null);
+ RenderNodeOpReflector renderNodeOpReflector =
+ reflector(RenderNodeOpReflector.class, renderNode);
+ Object displayListCanvas = renderNodeOpReflector.start(100, 100);
+ renderNodeOpReflector.end(displayListCanvas);
+ assertThat(renderNodeOpReflector.isValid()).isTrue();
+ }
+
+ @ForType(className = "android.view.RenderNode")
+ interface RenderNodeOpReflector {
+ @Static
+ Object create(String name, View owningView);
+
+ Object start(int width, int height);
+
+ void end(@WithType("android.view.DisplayListCanvas") Object displayListCanvas);
+
+ boolean isValid();
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRuntimeShaderTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRuntimeShaderTest.java
new file mode 100644
index 000000000..9f06cf59a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeRuntimeShaderTest.java
@@ -0,0 +1,81 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.S_V2;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static com.google.common.base.StandardSystemProperty.OS_NAME;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import android.graphics.RuntimeShader;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.Locale;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+@Config(minSdk = S)
+@RunWith(AndroidJUnit4.class)
+public class ShadowNativeRuntimeShaderTest {
+ static final String SKSL =
+ ""
+ + "uniform float2 in_origin;"
+ + "uniform float in_progress;\n"
+ + "uniform float in_maxRadius;\n"
+ + "uniform shader in_paintColor;\n"
+ + "float dist2(float2 p0, float2 pf) { return sqrt((pf.x - p0.x) * (pf.x - p0.x) + "
+ + "(pf.y - p0.y) * (pf.y - p0.y)); }\n"
+ + "float mod2(float a, float b) { return a - (b * floor(a / b)); }\n"
+ + "float rand(float2 src) { return fract(sin(dot(src.xy, float2(12.9898, 78.233)))"
+ + " * 43758.5453123); }\n"
+ + "float4 main(float2 p)\n"
+ + "{\n"
+ + " float fraction = in_progress;\n"
+ + " float2 fragCoord = p;//sk_FragCoord.xy;\n"
+ + " float maxDist = in_maxRadius;\n"
+ + " float fragDist = dist2(in_origin, fragCoord.xy);\n"
+ + " float circleRadius = maxDist * fraction;\n"
+ + " float colorVal = (fragDist - circleRadius) / maxDist;\n"
+ + " float d = fragDist < circleRadius \n"
+ + " ? 1. - abs(colorVal * 2. * smoothstep(0., 1., fraction)) \n"
+ + " : 1. - abs(colorVal * 3.);\n"
+ + " d = smoothstep(0., 1., d);\n"
+ + " float divider = 2.;\n"
+ + " float x = floor(fragCoord.x / divider);\n"
+ + " float y = floor(fragCoord.y / divider);\n"
+ + " float density = .95;\n"
+ + " d = rand(float2(x, y)) > density ? d : d * .2;\n"
+ + " d = d * rand(float2(fraction, x * y));\n"
+ + " float alpha = 1. - pow(fraction, 3.);\n"
+ + " return float4(sample(in_paintColor, p).rgb, d * alpha);\n"
+ + "}";
+
+ @Before
+ public void setup() {
+ // The native code behind RuntimeShader is currently not supported on Mac.
+ assume().that(OS_NAME.value().toLowerCase(Locale.US)).doesNotContain("mac");
+ }
+
+ @Config(minSdk = S, maxSdk = S_V2)
+ @Test
+ public void testConstructor() {
+ var unused =
+ ReflectionHelpers.callConstructor(
+ RuntimeShader.class,
+ ClassParameter.from(String.class, SKSL),
+ ClassParameter.from(boolean.class, false));
+ }
+
+ @Config(minSdk = TIRAMISU)
+ @Test
+ public void testConstructorT() {
+ var unused = new RuntimeShader(SKSL);
+ }
+
+ @Test
+ public void rippleShader_ctor() throws Exception {
+ ReflectionHelpers.callConstructor(Class.forName("android.graphics.drawable.RippleShader"));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeShaderTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeShaderTest.java
new file mode 100644
index 000000000..b25e3f13f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeShaderTest.java
@@ -0,0 +1,86 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Shader;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeShaderTest {
+ @Test
+ public void testConstructor() {
+ var unused = new Shader();
+ }
+
+ @Test
+ public void testAccessLocalMatrix() {
+ int width = 80;
+ int height = 120;
+ int[] color = new int[width * height];
+ Bitmap bitmap = Bitmap.createBitmap(color, width, height, Bitmap.Config.RGB_565);
+
+ Shader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+ Matrix m = new Matrix();
+
+ shader.setLocalMatrix(m);
+ assertFalse(shader.getLocalMatrix(m));
+
+ shader.setLocalMatrix(null);
+ assertFalse(shader.getLocalMatrix(m));
+ }
+
+ @Test
+ public void testMutateBaseObject() {
+ Shader shader = new Shader();
+ shader.setLocalMatrix(null);
+ }
+
+ @Test
+ @Config(minSdk = Q) // Cannot erase color Pre-Q
+ public void testGetSetLocalMatrix() {
+ Matrix skew10x20 = new Matrix();
+ skew10x20.setSkew(10, 20);
+
+ Matrix scale2x3 = new Matrix();
+ scale2x3.setScale(2, 3);
+
+ // setup shader
+ Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.RGB_565);
+ bitmap.eraseColor(Color.BLUE);
+ Shader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+
+ // get null
+ shader.setLocalMatrix(null);
+ Matrix paramMatrix = new Matrix(skew10x20);
+ assertFalse("shader should have no matrix set", shader.getLocalMatrix(paramMatrix));
+ assertEquals("matrix param not modified when no matrix set", skew10x20, paramMatrix);
+
+ // get nonnull
+ shader.setLocalMatrix(scale2x3);
+ assertTrue("shader should have matrix set", shader.getLocalMatrix(paramMatrix));
+ assertEquals("param matrix should be updated", scale2x3, paramMatrix);
+ }
+
+ @Test
+ public void testGetWithNullParam() {
+ Shader shader = new Shader();
+ Matrix matrix = new Matrix();
+ matrix.setScale(10, 10);
+ shader.setLocalMatrix(matrix);
+
+ assertThrows(NullPointerException.class, () -> shader.getLocalMatrix(null));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeStaticLayoutTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeStaticLayoutTest.java
new file mode 100644
index 000000000..5d77b1904
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeStaticLayoutTest.java
@@ -0,0 +1,1940 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics;
+
+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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Typeface;
+import android.os.Build.VERSION_CODES;
+import android.os.LocaleList;
+import android.text.Editable;
+import android.text.Layout;
+import android.text.Layout.Alignment;
+import android.text.PrecomputedText;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.StaticLayout;
+import android.text.TextDirectionHeuristic;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.text.style.LineBackgroundSpan;
+import android.text.style.LineHeightSpan;
+import android.text.style.ReplacementSpan;
+import android.text.style.StyleSpan;
+import android.text.style.TextAppearanceSpan;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.integrationtests.nativegraphics.testing.text.EditorState;
+
+/**
+ * Tests are derived from the <a
+ * http="https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:cts/tests/tests/text/src/android/text/cts/StaticLayoutTest.java">StaticLayoutTest
+ * CTS test</a>.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O)
+public class ShadowNativeStaticLayoutTest {
+
+ private static final float SPACE_MULTI = 1.0f;
+ private static final float SPACE_ADD = 0.0f;
+ private static final int DEFAULT_OUTER_WIDTH = 150;
+
+ private static final int LAST_LINE = 5;
+ private static final int LINE_COUNT = 6;
+ private static final int LARGER_THAN_LINE_COUNT = 50;
+
+ private static final String LOREM_IPSUM =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing "
+ + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad "
+ + "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
+ + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse "
+ + "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non "
+ + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
+
+ /* the first line must have one tab. the others not. totally 6 lines
+ */
+ private static final CharSequence LAYOUT_TEXT =
+ "CharSe\tq\nChar" + "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong";
+
+ private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence";
+
+ private static final int VERTICAL_BELOW_TEXT = 1000;
+
+ private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER;
+
+ private static final int ELLIPSIZE_WIDTH = 8;
+
+ private StaticLayout defaultLayout;
+ private TextPaint defaultPaint;
+
+ private static class TestingTextPaint extends TextPaint {
+ // need to have a subclass to ensure measurement happens in Java and not C++
+ }
+
+ @Before
+ public void setup() {
+ defaultPaint = new TextPaint();
+ defaultLayout = createDefaultStaticLayout();
+ }
+
+ private StaticLayout createDefaultStaticLayout() {
+ return new StaticLayout(
+ LAYOUT_TEXT,
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+ }
+
+ private StaticLayout createEllipsizeStaticLayout() {
+ return new StaticLayout(
+ LAYOUT_TEXT,
+ 0,
+ LAYOUT_TEXT.length(),
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true,
+ TextUtils.TruncateAt.MIDDLE,
+ ELLIPSIZE_WIDTH);
+ }
+
+ private StaticLayout createEllipsizeStaticLayout(
+ CharSequence text, TextUtils.TruncateAt ellipsize) {
+ return new StaticLayout(
+ text,
+ 0,
+ text.length(),
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true /* include pad */,
+ ellipsize,
+ ELLIPSIZE_WIDTH);
+ }
+
+ /** Constructor test */
+ @Test
+ @SuppressWarnings("CheckReturnValue")
+ public void testConstructor() {
+ new StaticLayout(
+ LAYOUT_TEXT,
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+
+ new StaticLayout(
+ LAYOUT_TEXT,
+ 0,
+ LAYOUT_TEXT.length(),
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+
+ new StaticLayout(
+ LAYOUT_TEXT,
+ 0,
+ LAYOUT_TEXT.length(),
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ false,
+ null,
+ 0);
+ }
+
+ @Test
+ public void testConstructorNull() {
+ assertThrows(
+ NullPointerException.class, () -> new StaticLayout(null, null, -1, null, 0, 0, true));
+ }
+
+ @Test
+ public void testBuilder() {
+ {
+ // Obtain.
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ StaticLayout layout = builder.build();
+ // Check values passed to obtain().
+ assertEquals(LAYOUT_TEXT, layout.getText());
+ assertEquals(defaultPaint, layout.getPaint());
+ assertEquals(DEFAULT_OUTER_WIDTH, layout.getWidth());
+ // Check default values.
+ assertEquals(Alignment.ALIGN_NORMAL, layout.getAlignment());
+ assertEquals(0.0f, layout.getSpacingAdd(), 0.0f);
+ assertEquals(1.0f, layout.getSpacingMultiplier(), 0.0f);
+ assertEquals(DEFAULT_OUTER_WIDTH, layout.getEllipsizedWidth());
+ }
+ {
+ // Obtain with null objects.
+ StaticLayout.Builder builder = StaticLayout.Builder.obtain(null, 0, 0, null, 0);
+ assertThrows(NullPointerException.class, builder::build);
+ }
+ {
+ // setText.
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setText(LAYOUT_TEXT_SINGLE_LINE);
+ StaticLayout layout = builder.build();
+ assertEquals(LAYOUT_TEXT_SINGLE_LINE, layout.getText());
+ }
+ {
+ // setAlignment.
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setAlignment(DEFAULT_ALIGN);
+ StaticLayout layout = builder.build();
+ assertEquals(DEFAULT_ALIGN, layout.getAlignment());
+ }
+ {
+ // setLineSpacing.
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setLineSpacing(1.0f, 2.0f);
+ StaticLayout layout = builder.build();
+ assertEquals(1.0f, layout.getSpacingAdd(), 0.0f);
+ assertEquals(2.0f, layout.getSpacingMultiplier(), 0.0f);
+ }
+ {
+ // setEllipsizedWidth and setEllipsize.
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setEllipsize(TruncateAt.END);
+ builder.setEllipsizedWidth(ELLIPSIZE_WIDTH);
+ StaticLayout layout = builder.build();
+ assertEquals(ELLIPSIZE_WIDTH, layout.getEllipsizedWidth());
+ assertEquals(DEFAULT_OUTER_WIDTH, layout.getWidth());
+ assertTrue(layout.getEllipsisCount(0) == 0);
+ assertTrue(layout.getEllipsisCount(5) > 0);
+ }
+ {
+ // setMaxLines.
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setMaxLines(1);
+ builder.setEllipsize(TruncateAt.END);
+ StaticLayout layout = builder.build();
+ assertTrue(layout.getEllipsisCount(0) > 0);
+ assertEquals(1, layout.getLineCount());
+ }
+ {
+ // Setter methods that cannot be directly tested.
+ // setBreakStrategy, setHyphenationFrequency, setIncludePad, and setIndents.
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
+ builder.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL);
+ builder.setIncludePad(true);
+ builder.setIndents(null, null);
+ StaticLayout layout = builder.build();
+ assertNotNull(layout);
+ }
+ }
+
+ @Config(minSdk = P) // TODO(hoisie): Fix in Android O/O_MR1
+ @Test
+ public void testSetLineSpacing_whereLineEndsWithNextLine() {
+ final float spacingAdd = 10f;
+ final float spacingMult = 3f;
+
+ // two lines of text, with line spacing, first line will have the spacing, but last line
+ // won't have the spacing
+ final String tmpText = "a\nb";
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ tmpText, 0, tmpText.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setLineSpacing(spacingAdd, spacingMult).setIncludePad(false);
+ final StaticLayout comparisonLayout = builder.build();
+
+ assertEquals(2, comparisonLayout.getLineCount());
+ final int heightWithLineSpacing =
+ comparisonLayout.getLineBottom(0) - comparisonLayout.getLineTop(0);
+ final int heightWithoutLineSpacing =
+ comparisonLayout.getLineBottom(1) - comparisonLayout.getLineTop(1);
+ assertTrue(heightWithLineSpacing > heightWithoutLineSpacing);
+
+ final String text = "a\n";
+ // build the layout to be tested
+ builder =
+ StaticLayout.Builder.obtain("a\n", 0, text.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setLineSpacing(spacingAdd, spacingMult).setIncludePad(false);
+ final StaticLayout layout = builder.build();
+
+ assertEquals(comparisonLayout.getLineCount(), layout.getLineCount());
+ assertEquals(heightWithLineSpacing, layout.getLineBottom(0) - layout.getLineTop(0));
+ assertEquals(heightWithoutLineSpacing, layout.getLineBottom(1) - layout.getLineTop(1));
+ }
+
+ @Test
+ public void testBuilder_setJustificationMode() {
+ StaticLayout.Builder builder =
+ StaticLayout.Builder.obtain(
+ LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), defaultPaint, DEFAULT_OUTER_WIDTH);
+ builder.setJustificationMode(Layout.JUSTIFICATION_MODE_INTER_WORD);
+ StaticLayout layout = builder.build();
+ // Hard to expect the justification result. Just make sure the final layout is created
+ // without causing any exceptions.
+ assertNotNull(layout);
+ }
+
+ /*
+ * Get the line number corresponding to the specified vertical position.
+ * If you ask for a position above 0, you get 0. above 0 means pixel above the fire line
+ * if you ask for a position in the range of the height, return the pixel in line
+ * if you ask for a position below the bottom of the text, you get the last line.
+ * Test 4 values containing -1, 0, normal number and > count
+ */
+ @Test
+ public void testGetLineForVertical() {
+ assertEquals(0, defaultLayout.getLineForVertical(-1));
+ assertEquals(0, defaultLayout.getLineForVertical(0));
+ assertTrue(defaultLayout.getLineForVertical(50) > 0);
+ assertEquals(LAST_LINE, defaultLayout.getLineForVertical(VERTICAL_BELOW_TEXT));
+ }
+
+ /** Return the number of lines of text in this layout. */
+ @Test
+ public void testGetLineCount() {
+ assertEquals(LINE_COUNT, defaultLayout.getLineCount());
+ }
+
+ /*
+ * Return the vertical position of the top of the specified line.
+ * If the specified line is one beyond the last line, returns the bottom of the last line.
+ * A line of text contains top and bottom in height. this method just get the top of a line
+ * Test 4 values containing -1, 0, normal number and > count
+ */
+ @Test
+ public void testGetLineTop() {
+ assertTrue(defaultLayout.getLineTop(0) >= 0);
+ assertTrue(defaultLayout.getLineTop(1) > defaultLayout.getLineTop(0));
+ }
+
+ @Test
+ public void testGetLineTopBeforeFirst() {
+ assertThrows(ArrayIndexOutOfBoundsException.class, () -> defaultLayout.getLineTop(-1));
+ }
+
+ @Test
+ public void testGetLineTopAfterLast() {
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> defaultLayout.getLineTop(LARGER_THAN_LINE_COUNT));
+ }
+
+ /**
+ * Return the descent of the specified line. This method just like getLineTop, descent means the
+ * bottom pixel of the line Test 4 values containing -1, 0, normal number and > count
+ */
+ @Test
+ public void testGetLineDescent() {
+ assertTrue(defaultLayout.getLineDescent(0) > 0);
+ assertTrue(defaultLayout.getLineDescent(1) > 0);
+ }
+
+ @Test
+ public void testGetLineDescentBeforeFirst() {
+ assertThrows(ArrayIndexOutOfBoundsException.class, () -> defaultLayout.getLineDescent(-1));
+ }
+
+ @Test
+ public void testGetLineDescentAfterLast() {
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> defaultLayout.getLineDescent(LARGER_THAN_LINE_COUNT));
+ }
+
+ /**
+ * Returns the primary directionality of the paragraph containing the specified line. By default,
+ * each line should be same
+ */
+ @Test
+ public void testGetParagraphDirection() {
+ assertEquals(defaultLayout.getParagraphDirection(0), defaultLayout.getParagraphDirection(1));
+ }
+
+ @Test
+ public void testGetParagraphDirectionBeforeFirst() {
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class, () -> defaultLayout.getParagraphDirection(-1));
+ }
+
+ @Test
+ public void testGetParagraphDirectionAfterLast() {
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> defaultLayout.getParagraphDirection(LARGER_THAN_LINE_COUNT));
+ }
+
+ /**
+ * Return the text offset of the beginning of the specified line. If the specified line is one
+ * beyond the last line, returns the end of the last line. Test 4 values containing -1, 0, normal
+ * number and > count Each line's offset must >= 0
+ */
+ @Test
+ public void testGetLineStart() {
+ assertTrue(defaultLayout.getLineStart(0) >= 0);
+ assertTrue(defaultLayout.getLineStart(1) >= 0);
+ }
+
+ @Test
+ public void testGetLineStartBeforeFirst() {
+ assertThrows(ArrayIndexOutOfBoundsException.class, () -> defaultLayout.getLineStart(-1));
+ }
+
+ @Test
+ public void testGetLineStartAfterLast() {
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> defaultLayout.getLineStart(LARGER_THAN_LINE_COUNT));
+ }
+
+ /*
+ * Returns whether the specified line contains one or more tabs.
+ */
+ @Test
+ public void testGetContainsTab() {
+ assertTrue(defaultLayout.getLineContainsTab(0));
+ assertFalse(defaultLayout.getLineContainsTab(1));
+ }
+
+ @Test
+ public void testGetContainsTabBeforeFirst() {
+ assertThrows(ArrayIndexOutOfBoundsException.class, () -> defaultLayout.getLineContainsTab(-1));
+ }
+
+ @Test
+ public void testGetContainsTabAfterLast() {
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> defaultLayout.getLineContainsTab(LARGER_THAN_LINE_COUNT));
+ }
+
+ /**
+ * Returns an array of directionalities for the specified line. The array alternates counts of
+ * characters in left-to-right and right-to-left segments of the line. We can not check the return
+ * value, for Directions's field is package private So only check it not null
+ */
+ @Test
+ public void testGetLineDirections() {
+ assertNotNull(defaultLayout.getLineDirections(0));
+ assertNotNull(defaultLayout.getLineDirections(1));
+ }
+
+ @Test
+ public void testGetLineDirectionsBeforeFirst() {
+ assertThrows(ArrayIndexOutOfBoundsException.class, () -> defaultLayout.getLineDirections(-1));
+ }
+
+ @Test
+ public void testGetLineDirectionsAfterLast() {
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> defaultLayout.getLineDirections(LARGER_THAN_LINE_COUNT));
+ }
+
+ /**
+ * Returns the (negative) number of extra pixels of ascent padding in the top line of the Layout.
+ */
+ @Test
+ public void testGetTopPadding() {
+ assertTrue(defaultLayout.getTopPadding() < 0);
+ }
+
+ /** Returns the number of extra pixels of descent padding in the bottom line of the Layout. */
+ @Test
+ public void testGetBottomPadding() {
+ assertTrue(defaultLayout.getBottomPadding() > 0);
+ }
+
+ /*
+ * Returns the number of characters to be ellipsized away, or 0 if no ellipsis is to take place.
+ * So each line must >= 0
+ */
+ @Test
+ public void testGetEllipsisCount() {
+ // Multilines (6 lines) and TruncateAt.START so no ellipsis at all
+ defaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT, TextUtils.TruncateAt.MIDDLE);
+
+ assertTrue(defaultLayout.getEllipsisCount(0) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(1) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(2) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(3) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(4) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(5) == 0);
+
+ assertThrows(ArrayIndexOutOfBoundsException.class, () -> defaultLayout.getEllipsisCount(-1));
+
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> defaultLayout.getEllipsisCount(LARGER_THAN_LINE_COUNT));
+
+ // Multilines (6 lines) and TruncateAt.MIDDLE so no ellipsis at all
+ defaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT, TextUtils.TruncateAt.MIDDLE);
+
+ assertTrue(defaultLayout.getEllipsisCount(0) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(1) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(2) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(3) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(4) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(5) == 0);
+
+ // Multilines (6 lines) and TruncateAt.END so ellipsis only on the last line
+ defaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT, TextUtils.TruncateAt.END);
+
+ assertTrue(defaultLayout.getEllipsisCount(0) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(1) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(2) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(3) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(4) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(5) > 0);
+
+ // Multilines (6 lines) and TruncateAt.MARQUEE so ellipsis only on the last line
+ defaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT, TextUtils.TruncateAt.END);
+
+ assertTrue(defaultLayout.getEllipsisCount(0) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(1) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(2) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(3) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(4) == 0);
+ assertTrue(defaultLayout.getEllipsisCount(5) > 0);
+ }
+
+ /*
+ * Return the offset of the first character to be ellipsized away
+ * relative to the start of the line.
+ * (So 0 if the beginning of the line is ellipsized, not getLineStart().)
+ */
+ @Test
+ public void testGetEllipsisStart() {
+ defaultLayout = createEllipsizeStaticLayout();
+ assertTrue(defaultLayout.getEllipsisStart(0) >= 0);
+ assertTrue(defaultLayout.getEllipsisStart(1) >= 0);
+
+ assertThrows(ArrayIndexOutOfBoundsException.class, () -> defaultLayout.getEllipsisStart(-1));
+ assertThrows(
+ ArrayIndexOutOfBoundsException.class,
+ () -> defaultLayout.getEllipsisStart(LARGER_THAN_LINE_COUNT));
+ }
+
+ /*
+ * Return the width to which this Layout is ellipsizing
+ * or getWidth() if it is not doing anything special.
+ * The constructor's Argument TextUtils.TruncateAt defines which EllipsizedWidth to use
+ * ellipsizedWidth if argument is not null
+ * outerWidth if argument is null
+ */
+ @Test
+ public void testGetEllipsizedWidth() {
+ int ellipsizedWidth = 60;
+ int outerWidth = 100;
+ StaticLayout layout =
+ new StaticLayout(
+ LAYOUT_TEXT,
+ 0,
+ LAYOUT_TEXT.length(),
+ defaultPaint,
+ outerWidth,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ false,
+ TextUtils.TruncateAt.END,
+ ellipsizedWidth);
+ assertEquals(ellipsizedWidth, layout.getEllipsizedWidth());
+
+ layout =
+ new StaticLayout(
+ LAYOUT_TEXT,
+ 0,
+ LAYOUT_TEXT.length(),
+ defaultPaint,
+ outerWidth,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ false,
+ null,
+ ellipsizedWidth);
+ assertEquals(outerWidth, layout.getEllipsizedWidth());
+ }
+
+ /**
+ * scenario description: 1. set the text. 2. change the text 3. Check the text won't change to the
+ * StaticLayout
+ */
+ @Test
+ public void testImmutableStaticLayout() {
+ Editable editable = Editable.Factory.getInstance().newEditable("123\t\n555");
+ StaticLayout layout =
+ new StaticLayout(
+ editable,
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+
+ assertEquals(2, layout.getLineCount());
+ assertTrue(defaultLayout.getLineContainsTab(0));
+
+ // change the text
+ editable.delete(0, editable.length() - 1);
+
+ assertEquals(2, layout.getLineCount());
+ assertTrue(layout.getLineContainsTab(0));
+ }
+
+ // String wrapper for testing not well known implementation of CharSequence.
+ private static class FakeCharSequence implements CharSequence {
+ private String str;
+
+ public FakeCharSequence(String str) {
+ this.str = str;
+ }
+
+ @Override
+ public char charAt(int index) {
+ return str.charAt(index);
+ }
+
+ @Override
+ public int length() {
+ return str.length();
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ return str.subSequence(start, end);
+ }
+
+ @Override
+ public String toString() {
+ return str;
+ }
+ }
+
+ private List<CharSequence> buildTestCharSequences(String testString, Normalizer.Form[] forms) {
+ List<CharSequence> result = new ArrayList<>();
+
+ List<String> normalizedStrings = new ArrayList<>();
+ for (Normalizer.Form form : forms) {
+ normalizedStrings.add(Normalizer.normalize(testString, form));
+ }
+
+ for (String str : normalizedStrings) {
+ result.add(str);
+ result.add(new SpannedString(str));
+ result.add(new SpannableString(str));
+ result.add(new SpannableStringBuilder(str)); // as a GraphicsOperations implementation.
+ result.add(new FakeCharSequence(str)); // as a not well known implementation.
+ }
+ return result;
+ }
+
+ private String buildTestMessage(CharSequence seq) {
+ String normalized;
+ if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) {
+ normalized = "NFC";
+ } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) {
+ normalized = "NFD";
+ } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) {
+ normalized = "NFKC";
+ } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) {
+ normalized = "NFKD";
+ } else {
+ throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD");
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < seq.length(); ++i) {
+ builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i))));
+ }
+
+ return "testString: \""
+ + seq.toString()
+ + "\"["
+ + builder.toString()
+ + "]"
+ + ", class: "
+ + seq.getClass().getName()
+ + ", Normalization: "
+ + normalized;
+ }
+
+ @Test
+ public void testGetOffset_ascii() {
+ String[] testStrings = {"abcde", "ab\ncd", "ab\tcd", "ab\n\nc", "ab\n\tc"};
+
+ for (String testString : testStrings) {
+ for (CharSequence seq : buildTestCharSequences(testString, Normalizer.Form.values())) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq,
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
+
+ assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
+ }
+ }
+
+ String testString = "ab\r\nde";
+ for (CharSequence seq : buildTestCharSequences(testString, Normalizer.Form.values())) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq, defaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
+ assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
+
+ assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(6));
+ }
+ }
+
+ @Test
+ public void testGetOffset_unicode() {
+ String[] testStrings =
+ new String[] {
+ // Cyrillic alphabets.
+ "\u0410\u0411\u0412\u0413\u0414",
+ // Japanese Hiragana Characters.
+ "\u3042\u3044\u3046\u3048\u304A",
+ };
+
+ for (String testString : testStrings) {
+ for (CharSequence seq : buildTestCharSequences(testString, Normalizer.Form.values())) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq,
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
+
+ assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
+ }
+ }
+ }
+
+ @Test
+ public void testGetOffset_unicode_normalization() {
+ // "A" with acute, circumflex, tilde, diaeresis, ring above.
+ String testString = "\u00C1\u00C2\u00C3\u00C4\u00C5";
+ Normalizer.Form[] oneUnicodeForms = {Normalizer.Form.NFC, Normalizer.Form.NFKC};
+ for (CharSequence seq : buildTestCharSequences(testString, oneUnicodeForms)) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq, defaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
+
+ assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
+ }
+
+ Normalizer.Form[] twoUnicodeForms = {Normalizer.Form.NFD, Normalizer.Form.NFKD};
+ for (CharSequence seq : buildTestCharSequences(testString, twoUnicodeForms)) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq, defaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(6));
+ assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
+ assertEquals(testLabel, 6, layout.getOffsetToLeftOf(8));
+ assertEquals(testLabel, 8, layout.getOffsetToLeftOf(9));
+ assertEquals(testLabel, 8, layout.getOffsetToLeftOf(10));
+
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
+ assertEquals(testLabel, 8, layout.getOffsetToRightOf(6));
+ assertEquals(testLabel, 8, layout.getOffsetToRightOf(7));
+ assertEquals(testLabel, 10, layout.getOffsetToRightOf(8));
+ assertEquals(testLabel, 10, layout.getOffsetToRightOf(9));
+ assertEquals(testLabel, 10, layout.getOffsetToRightOf(10));
+ }
+ }
+
+ @Test
+ public void testGetOffset_unicode_surrogatePairs() {
+ // Emoticons for surrogate pairs tests.
+ String testString = "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03\uD83D\uDE04";
+ for (CharSequence seq : buildTestCharSequences(testString, Normalizer.Form.values())) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq, defaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(6));
+ assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
+ assertEquals(testLabel, 6, layout.getOffsetToLeftOf(8));
+ assertEquals(testLabel, 8, layout.getOffsetToLeftOf(9));
+ assertEquals(testLabel, 8, layout.getOffsetToLeftOf(10));
+
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
+ assertEquals(testLabel, 8, layout.getOffsetToRightOf(6));
+ assertEquals(testLabel, 8, layout.getOffsetToRightOf(7));
+ assertEquals(testLabel, 10, layout.getOffsetToRightOf(8));
+ assertEquals(testLabel, 10, layout.getOffsetToRightOf(9));
+ assertEquals(testLabel, 10, layout.getOffsetToRightOf(10));
+ }
+ }
+
+ @Test
+ public void testGetOffset_unicode_thai() {
+ // Thai Characters. The expected cursorable boundary is
+ // | \u0E02 | \u0E2D | \u0E1A | \u0E04\u0E38 | \u0E13 |
+ String testString = "\u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13";
+ for (CharSequence seq : buildTestCharSequences(testString, Normalizer.Form.values())) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq, defaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 3, layout.getOffsetToLeftOf(5));
+ assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
+
+ assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(6));
+ }
+ }
+
+ @Config(minSdk = P) // TODO(hoisie): Fix in Android O/O_MR1
+ @Test
+ public void testGetOffset_unicode_arabic() {
+ // Arabic Characters. The expected cursorable boundary is
+ // | \u0623 \u064F | \u0633 \u0652 | \u0631 \u064E | \u0629 \u064C |";
+ String testString = "\u0623\u064F\u0633\u0652\u0631\u064E\u0629\u064C";
+
+ Normalizer.Form[] oneUnicodeForms = {Normalizer.Form.NFC, Normalizer.Form.NFKC};
+ for (CharSequence seq : buildTestCharSequences(testString, oneUnicodeForms)) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq, defaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 6, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 6, layout.getOffsetToLeftOf(5));
+ assertEquals(testLabel, 8, layout.getOffsetToLeftOf(6));
+ assertEquals(testLabel, 8, layout.getOffsetToLeftOf(7));
+ assertEquals(testLabel, 8, layout.getOffsetToLeftOf(8));
+
+ assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 0, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(5));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(6));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(7));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(8));
+ }
+ }
+
+ @Config(minSdk = P) // TODO(hoisie): Fix in Android O/O_MR1
+ @Test
+ public void testGetOffset_unicode_bidi() {
+ // String having RTL characters and LTR characters
+
+ // LTR Context
+ // The first and last two characters are LTR characters.
+ String testString = "\u0061\u0062\u05DE\u05E1\u05E2\u0063\u0064";
+ // Logical order: [L1] [L2] [R1] [R2] [R3] [L3] [L4]
+ // 0 1 2 3 4 5 6 7
+ // Display order: [L1] [L2] [R3] [R2] [R1] [L3] [L4]
+ // 0 1 2 4 3 5 6 7
+ // [L?] means ?th LTR character and [R?] means ?th RTL character.
+ for (CharSequence seq : buildTestCharSequences(testString, Normalizer.Form.values())) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq, defaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 3, layout.getOffsetToLeftOf(5));
+ assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
+ assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
+
+ assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 3, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
+ assertEquals(testLabel, 7, layout.getOffsetToRightOf(6));
+ assertEquals(testLabel, 7, layout.getOffsetToRightOf(7));
+ }
+
+ // RTL Context
+ // The first and last two characters are RTL characters.
+ String testString2 = "\u05DE\u05E1\u0063\u0064\u0065\u05DE\u05E1";
+ // Logical order: [R1] [R2] [L1] [L2] [L3] [R3] [R4]
+ // 0 1 2 3 4 5 6 7
+ // Display order: [R4] [R3] [L1] [L2] [L3] [R2] [R1]
+ // 7 6 5 3 4 2 1 0
+ // [L?] means ?th LTR character and [R?] means ?th RTL character.
+ for (CharSequence seq : buildTestCharSequences(testString2, Normalizer.Form.values())) {
+ StaticLayout layout =
+ new StaticLayout(
+ seq, defaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+
+ String testLabel = buildTestMessage(seq);
+
+ assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0));
+ assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
+ assertEquals(testLabel, 4, layout.getOffsetToLeftOf(2));
+ assertEquals(testLabel, 5, layout.getOffsetToLeftOf(3));
+ assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
+ assertEquals(testLabel, 6, layout.getOffsetToLeftOf(5));
+ assertEquals(testLabel, 7, layout.getOffsetToLeftOf(6));
+ assertEquals(testLabel, 7, layout.getOffsetToLeftOf(7));
+
+ assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
+ assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
+ assertEquals(testLabel, 1, layout.getOffsetToRightOf(2));
+ assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
+ assertEquals(testLabel, 2, layout.getOffsetToRightOf(4));
+ assertEquals(testLabel, 3, layout.getOffsetToRightOf(5));
+ assertEquals(testLabel, 5, layout.getOffsetToRightOf(6));
+ assertEquals(testLabel, 6, layout.getOffsetToRightOf(7));
+ }
+ }
+
+ private void moveCursorToRightCursorableOffset(EditorState state) {
+ assertEquals("The editor has selection", state.selectionStart, state.selectionEnd);
+ StaticLayout layout =
+ StaticLayout.Builder.obtain(
+ state.text, 0, state.text.length(), defaultPaint, DEFAULT_OUTER_WIDTH)
+ .build();
+ final int newOffset = layout.getOffsetToRightOf(state.selectionStart);
+ state.selectionStart = state.selectionEnd = newOffset;
+ }
+
+ private void moveCursorToLeftCursorableOffset(EditorState state) {
+ assertEquals("The editor has selection", state.selectionStart, state.selectionEnd);
+ StaticLayout layout =
+ StaticLayout.Builder.obtain(
+ state.text, 0, state.text.length(), defaultPaint, DEFAULT_OUTER_WIDTH)
+ .build();
+ final int newOffset = layout.getOffsetToLeftOf(state.selectionStart);
+ state.selectionStart = state.selectionEnd = newOffset;
+ }
+
+ @Test
+ public void testGetOffset_emoji() {
+ EditorState state = new EditorState();
+
+ // Emojis
+ // U+00A9 is COPYRIGHT SIGN.
+ state.setByString("| U+00A9 U+00A9 U+00A9");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+00A9 | U+00A9 U+00A9");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+00A9 U+00A9 | U+00A9");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+00A9 U+00A9 U+00A9 |");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+00A9 U+00A9 U+00A9 |");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("U+00A9 U+00A9 | U+00A9");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("U+00A9 | U+00A9 U+00A9");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| U+00A9 U+00A9 U+00A9");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| U+00A9 U+00A9 U+00A9");
+
+ // Surrogate pairs
+ // U+1F468 is MAN.
+ state.setByString("| U+1F468 U+1F468 U+1F468");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+1F468 | U+1F468 U+1F468");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+1F468 U+1F468 | U+1F468");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+1F468 U+1F468 U+1F468 |");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+1F468 U+1F468 U+1F468 |");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("U+1F468 U+1F468 | U+1F468");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("U+1F468 | U+1F468 U+1F468");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| U+1F468 U+1F468 U+1F468");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| U+1F468 U+1F468 U+1F468");
+
+ // Keycaps
+ // U+20E3 is COMBINING ENCLOSING KEYCAP.
+ state.setByString("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("'1' U+20E3 | '1' U+20E3 '1' U+20E3");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("'1' U+20E3 '1' U+20E3 | '1' U+20E3");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("'1' U+20E3 '1' U+20E3 '1' U+20E3 |");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("'1' U+20E3 '1' U+20E3 '1' U+20E3 |");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("'1' U+20E3 '1' U+20E3 | '1' U+20E3");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("'1' U+20E3 | '1' U+20E3 '1' U+20E3");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
+
+ // Variation selectors
+ // U+00A9 is COPYRIGHT SIGN, U+FE0E is VARIATION SELECTOR-15. U+FE0F is VARIATION
+ // SELECTOR-16.
+ state.setByString("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+00A9 U+FE0E | U+00A9 U+FE0F U+00A9 U+FE0E");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F | U+00A9 U+FE0E");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E |");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E |");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F | U+00A9 U+FE0E");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("U+00A9 U+FE0E | U+00A9 U+FE0F U+00A9 U+FE0E");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
+
+ // TODO(hoisie): Investigate why this fails in P
+ if (RuntimeEnvironment.getApiLevel() > P) {
+ // Keycap + variation selector
+ state.setByString("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("'1' U+FE0F U+20E3 | '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 | '1' U+FE0F U+20E3");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 |");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 |");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 | '1' U+FE0F U+20E3");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("'1' U+FE0F U+20E3 | '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
+ }
+
+ // Flags
+ // U+1F1E6 U+1F1E8 is Ascension Island flag.
+ state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 |");
+ moveCursorToRightCursorableOffset(state);
+ state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 |");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
+ moveCursorToLeftCursorableOffset(state);
+ state.assertEquals("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
+ }
+
+ @Test
+ public void testGetOffsetForHorizontal_multilines() {
+ // Emoticons for surrogate pairs tests.
+ String testString = "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03\uD83D\uDE04";
+ final float width = defaultPaint.measureText(testString, 0, 6);
+ StaticLayout layout =
+ new StaticLayout(
+ testString, defaultPaint, (int) width, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
+ // We expect the line break to be after the third emoticon, but we allow flexibility of the
+ // line break algorithm as long as the break is within the string. These other cases might
+ // happen if for example the font has kerning between emoticons.
+ final int lineBreakOffset = layout.getOffsetForHorizontal(1, 0.0f);
+ assertEquals(0, layout.getLineForOffset(lineBreakOffset - 1));
+
+ assertEquals(0, layout.getOffsetForHorizontal(0, 0.0f));
+ assertEquals(lineBreakOffset - 2, layout.getOffsetForHorizontal(0, width));
+ assertEquals(lineBreakOffset - 2, layout.getOffsetForHorizontal(0, width * 2));
+
+ final int lineCount = layout.getLineCount();
+ assertEquals(testString.length(), layout.getOffsetForHorizontal(lineCount - 1, width));
+ assertEquals(testString.length(), layout.getOffsetForHorizontal(lineCount - 1, width * 2));
+ }
+
+ @Config(minSdk = P) // TODO(hoisie): Fix in Android O/O_MR1
+ @Test
+ public void testIsRtlCharAt() {
+ {
+ String testString = "ab(\u0623\u0624)c\u0625";
+ StaticLayout layout =
+ new StaticLayout(
+ testString,
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+
+ assertFalse(layout.isRtlCharAt(0));
+ assertFalse(layout.isRtlCharAt(1));
+ assertFalse(layout.isRtlCharAt(2));
+ assertTrue(layout.isRtlCharAt(3));
+ assertTrue(layout.isRtlCharAt(4));
+ assertFalse(layout.isRtlCharAt(5));
+ assertFalse(layout.isRtlCharAt(6));
+ assertTrue(layout.isRtlCharAt(7));
+ }
+ {
+ String testString = "\u0623\u0624(ab)\u0625c";
+ StaticLayout layout =
+ new StaticLayout(
+ testString,
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+
+ assertTrue(layout.isRtlCharAt(0));
+ assertTrue(layout.isRtlCharAt(1));
+ assertTrue(layout.isRtlCharAt(2));
+ assertFalse(layout.isRtlCharAt(3));
+ assertFalse(layout.isRtlCharAt(4));
+ assertTrue(layout.isRtlCharAt(5));
+ assertTrue(layout.isRtlCharAt(6));
+ assertFalse(layout.isRtlCharAt(7));
+ assertFalse(layout.isRtlCharAt(8));
+ }
+ }
+
+ @Config(minSdk = P) // TODO(hoisie): Fix in Android O/O_MR1
+ @Test
+ public void testGetHorizontal() {
+ String testString = "abc\u0623\u0624\u0625def";
+ StaticLayout layout =
+ new StaticLayout(
+ testString,
+ defaultPaint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+
+ assertEquals(layout.getPrimaryHorizontal(0), layout.getSecondaryHorizontal(0), 0.0f);
+ assertTrue(layout.getPrimaryHorizontal(0) < layout.getPrimaryHorizontal(3));
+ assertTrue(layout.getPrimaryHorizontal(3) < layout.getSecondaryHorizontal(3));
+ assertTrue(layout.getPrimaryHorizontal(4) < layout.getSecondaryHorizontal(3));
+ assertEquals(layout.getPrimaryHorizontal(4), layout.getSecondaryHorizontal(4), 0.0f);
+ assertEquals(layout.getPrimaryHorizontal(3), layout.getSecondaryHorizontal(6), 0.0f);
+ assertEquals(layout.getPrimaryHorizontal(6), layout.getSecondaryHorizontal(3), 0.0f);
+ assertEquals(layout.getPrimaryHorizontal(7), layout.getSecondaryHorizontal(7), 0.0f);
+ }
+
+ @Test
+ public void testVeryLargeString() {
+ final int maxCount = 1 << 20;
+ final int wordSize = 32;
+ char[] longText = new char[maxCount];
+ for (int n = 0; n < maxCount; n++) {
+ longText[n] = (n % wordSize) == 0 ? ' ' : 'm';
+ }
+ String longTextString = new String(longText);
+ TextPaint paint = new TestingTextPaint();
+ StaticLayout layout =
+ new StaticLayout(
+ longTextString,
+ paint,
+ DEFAULT_OUTER_WIDTH,
+ DEFAULT_ALIGN,
+ SPACE_MULTI,
+ SPACE_ADD,
+ true);
+ assertNotNull(layout);
+ // In Android O and Android O_MR1, StaticLayout.addMeasuredRun will get called for this very
+ // long line, which will result in the recycle mechanism being triggered, as the number
+ // of lines is greater than 16.
+ assertThat(layout.getLineCount()).isGreaterThan(16);
+ }
+
+ @Test
+ public void testNoCrashWhenWordStyleOverlap() {
+ // test case where word boundary overlaps multiple style spans
+ SpannableStringBuilder text = new SpannableStringBuilder("word boundaries, overlap style");
+ // span covers "boundaries"
+ text.setSpan(
+ new StyleSpan(Typeface.BOLD),
+ "word ".length(),
+ "word boundaries".length(),
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ defaultPaint.setTextLocale(Locale.US);
+ StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), defaultPaint, DEFAULT_OUTER_WIDTH)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) // enable hyphenation
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .build();
+ assertNotNull(layout);
+ }
+
+ @Test
+ public void testRespectingIndentsOnEllipsizedText() {
+ // test case where word boundary overlaps multiple style spans
+ final String text = "words with indents";
+
+ // +1 to ensure that we won't wrap in the normal case
+ int textWidth = (int) (defaultPaint.measureText(text) + 1);
+ StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), defaultPaint, textWidth)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) // enable hyphenation
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .setEllipsize(TruncateAt.END)
+ .setEllipsizedWidth(textWidth)
+ .setMaxLines(1)
+ .setIndents(null, new int[] {20})
+ .build();
+ assertTrue(layout.getEllipsisStart(0) != 0);
+ }
+
+ @Test
+ public void testGetPrimary_shouldFail_whenOffsetIsOutOfBounds_withSpannable() {
+ final String text = "1\n2\n3";
+ final SpannableString spannable = new SpannableString(text);
+ spannable.setSpan(new Object(), 0, text.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+ final Layout layout =
+ StaticLayout.Builder.obtain(
+ spannable, 0, spannable.length(), defaultPaint, Integer.MAX_VALUE - 1)
+ .setMaxLines(2)
+ .setEllipsize(TruncateAt.END)
+ .build();
+ assertThrows(
+ IndexOutOfBoundsException.class,
+ () -> layout.getPrimaryHorizontal(layout.getText().length()));
+ }
+
+ @Test
+ public void testGetPrimary_shouldFail_whenOffsetIsOutOfBounds_withString() {
+ final String text = "1\n2\n3";
+ final Layout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), defaultPaint, Integer.MAX_VALUE - 1)
+ .setMaxLines(2)
+ .setEllipsize(TruncateAt.END)
+ .build();
+ assertThrows(
+ IndexOutOfBoundsException.class,
+ () -> layout.getPrimaryHorizontal(layout.getText().length()));
+ }
+
+ @Test
+ public void testNegativeWidth() {
+ StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5)
+ .setIndents(new int[] {10}, new int[] {10})
+ .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)
+ .build();
+ StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5)
+ .setIndents(new int[] {10}, new int[] {10})
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .build();
+ StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5)
+ .setIndents(new int[] {10}, new int[] {10})
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
+ .build();
+ }
+
+ @Test
+ public void testGetLineMax() {
+ final float wholeWidth = defaultPaint.measureText(LOREM_IPSUM);
+ final int lineWidth = (int) (wholeWidth / 10.0f); // Make 10 lines per paragraph.
+ final String multiParaTestString =
+ LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM;
+ final Layout layout =
+ StaticLayout.Builder.obtain(
+ multiParaTestString, 0, multiParaTestString.length(), defaultPaint, lineWidth)
+ .build();
+ for (int i = 0; i < layout.getLineCount(); i++) {
+ assertTrue(layout.getLineMax(i) <= lineWidth);
+ }
+ }
+
+ @Test
+ public void testIndent() {
+ final float wholeWidth = defaultPaint.measureText(LOREM_IPSUM);
+ final int lineWidth = (int) (wholeWidth / 10.0f); // Make 10 lines per paragraph.
+ final int indentWidth = (int) (lineWidth * 0.3f); // Make 30% indent.
+ final String multiParaTestString =
+ LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM;
+ final Layout layout =
+ StaticLayout.Builder.obtain(
+ multiParaTestString, 0, multiParaTestString.length(), defaultPaint, lineWidth)
+ .setIndents(new int[] {indentWidth}, null)
+ .build();
+ for (int i = 0; i < layout.getLineCount(); i++) {
+ assertTrue(layout.getLineMax(i) <= lineWidth - indentWidth);
+ }
+ }
+
+ @CanIgnoreReturnValue
+ private static Bitmap drawToBitmap(Layout l) {
+ final Bitmap bmp = Bitmap.createBitmap(l.getWidth(), l.getHeight(), Bitmap.Config.RGB_565);
+ final Canvas c = new Canvas(bmp);
+
+ c.save();
+ c.translate(0, 0);
+ l.draw(c);
+ c.restore();
+ return bmp;
+ }
+
+ private static String textPaintToString(TextPaint p) {
+ return "{"
+ + "mTextSize="
+ + p.getTextSize()
+ + ", "
+ + "mTextSkewX="
+ + p.getTextSkewX()
+ + ", "
+ + "mTextScaleX="
+ + p.getTextScaleX()
+ + ", "
+ + "mLetterSpacing="
+ + p.getLetterSpacing()
+ + ", "
+ + "mFlags="
+ + p.getFlags()
+ + ", "
+ + "mTextLocales="
+ + p.getTextLocales()
+ + ", "
+ + "mFontVariationSettings="
+ + p.getFontVariationSettings()
+ + ", "
+ + "mTypeface="
+ + p.getTypeface()
+ + ", "
+ + "mFontFeatureSettings="
+ + p.getFontFeatureSettings()
+ + "}";
+ }
+
+ private static String directionToString(TextDirectionHeuristic dir) {
+ if (dir == TextDirectionHeuristics.LTR) {
+ return "LTR";
+ } else if (dir == TextDirectionHeuristics.RTL) {
+ return "RTL";
+ } else if (dir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
+ return "FIRSTSTRONG_LTR";
+ } else if (dir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
+ return "FIRSTSTRONG_RTL";
+ } else if (dir == TextDirectionHeuristics.ANYRTL_LTR) {
+ return "ANYRTL_LTR";
+ } else {
+ throw new RuntimeException("Unknown Direction");
+ }
+ }
+
+ static class LayoutParam {
+ final int strategy;
+ final int frequency;
+ final TextPaint paint;
+ final TextDirectionHeuristic dir;
+
+ LayoutParam(int strategy, int frequency, TextPaint paint, TextDirectionHeuristic dir) {
+ this.strategy = strategy;
+ this.frequency = frequency;
+ this.paint = new TextPaint(paint);
+ this.dir = dir;
+ }
+
+ @Override
+ public String toString() {
+ return "{"
+ + "mStrategy="
+ + strategy
+ + ", "
+ + "mFrequency="
+ + frequency
+ + ", "
+ + "mPaint="
+ + textPaintToString(paint)
+ + ", "
+ + "mDir="
+ + directionToString(dir)
+ + "}";
+ }
+
+ Layout getLayout(CharSequence text, int width) {
+ return StaticLayout.Builder.obtain(text, 0, text.length(), paint, width)
+ .setBreakStrategy(strategy)
+ .setHyphenationFrequency(frequency)
+ .setTextDirection(dir)
+ .build();
+ }
+
+ PrecomputedText getPrecomputedText(CharSequence text) {
+ PrecomputedText.Params param =
+ new PrecomputedText.Params.Builder(paint)
+ .setBreakStrategy(strategy)
+ .setHyphenationFrequency(frequency)
+ .setTextDirection(dir)
+ .build();
+ return PrecomputedText.create(text, param);
+ }
+ }
+
+ void assertSameStaticLayout(
+ CharSequence text, LayoutParam measuredTextParam, LayoutParam staticLayoutParam) {
+ String msg =
+ "StaticLayout for "
+ + staticLayoutParam
+ + " with PrecomputedText"
+ + " created with "
+ + measuredTextParam
+ + " must output the same BMP.";
+
+ final float wholeWidth = defaultPaint.measureText(text.toString());
+ final int lineWidth = (int) (wholeWidth / 10.0f); // Make 10 lines per paragraph.
+
+ // Static layout parameter should be used for the final output.
+ final Layout expectedLayout = staticLayoutParam.getLayout(text, lineWidth);
+
+ final PrecomputedText mt = measuredTextParam.getPrecomputedText(text);
+ final Layout resultLayout =
+ StaticLayout.Builder.obtain(mt, 0, mt.length(), staticLayoutParam.paint, lineWidth)
+ .setBreakStrategy(staticLayoutParam.strategy)
+ .setHyphenationFrequency(staticLayoutParam.frequency)
+ .setTextDirection(staticLayoutParam.dir)
+ .build();
+
+ assertEquals(msg, expectedLayout.getHeight(), resultLayout.getHeight(), 0.0f);
+
+ final Bitmap expectedBMP = drawToBitmap(expectedLayout);
+ final Bitmap resultBMP = drawToBitmap(resultLayout);
+
+ assertTrue(msg, resultBMP.sameAs(expectedBMP));
+ }
+
+ @Config(minSdk = P) // PrecomputedText was added in P
+ @Test
+ public void testPrecomputedText() {
+ int[] breaks = {
+ Layout.BREAK_STRATEGY_SIMPLE,
+ Layout.BREAK_STRATEGY_HIGH_QUALITY,
+ Layout.BREAK_STRATEGY_BALANCED,
+ };
+
+ int[] frequencies = {
+ Layout.HYPHENATION_FREQUENCY_NORMAL,
+ Layout.HYPHENATION_FREQUENCY_FULL,
+ Layout.HYPHENATION_FREQUENCY_NONE,
+ };
+
+ TextDirectionHeuristic[] dirs = {
+ TextDirectionHeuristics.LTR,
+ TextDirectionHeuristics.RTL,
+ TextDirectionHeuristics.FIRSTSTRONG_LTR,
+ TextDirectionHeuristics.FIRSTSTRONG_RTL,
+ TextDirectionHeuristics.ANYRTL_LTR,
+ };
+
+ float[] textSizes = {8.0f, 16.0f, 32.0f};
+
+ LocaleList[] locales = {
+ LocaleList.forLanguageTags("en-US"),
+ LocaleList.forLanguageTags("ja-JP"),
+ LocaleList.forLanguageTags("en-US,ja-JP"),
+ };
+
+ TextPaint paint = new TextPaint();
+
+ // If the PrecomputedText is created with the same argument of the StaticLayout, generate
+ // the same bitmap.
+ for (int b : breaks) {
+ for (int f : frequencies) {
+ for (TextDirectionHeuristic dir : dirs) {
+ for (float textSize : textSizes) {
+ for (LocaleList locale : locales) {
+ paint.setTextSize(textSize);
+ paint.setTextLocales(locale);
+
+ assertSameStaticLayout(
+ LOREM_IPSUM,
+ new LayoutParam(b, f, paint, dir),
+ new LayoutParam(b, f, paint, dir));
+ }
+ }
+ }
+ }
+ }
+
+ // If the parameters are different, the output of the static layout must be
+ // same bitmap.
+ for (int bi = 0; bi < breaks.length; bi++) {
+ for (int fi = 0; fi < frequencies.length; fi++) {
+ for (int diri = 0; diri < dirs.length; diri++) {
+ for (int sizei = 0; sizei < textSizes.length; sizei++) {
+ for (int localei = 0; localei < locales.length; localei++) {
+ TextPaint p1 = new TextPaint();
+ TextPaint p2 = new TextPaint();
+
+ p1.setTextSize(textSizes[sizei]);
+ p2.setTextSize(textSizes[(sizei + 1) % textSizes.length]);
+
+ p1.setTextLocales(locales[localei]);
+ p2.setTextLocales(locales[(localei + 1) % locales.length]);
+
+ int b1 = breaks[bi];
+ int b2 = breaks[(bi + 1) % breaks.length];
+
+ int f1 = frequencies[fi];
+ int f2 = frequencies[(fi + 1) % frequencies.length];
+
+ TextDirectionHeuristic dir1 = dirs[diri];
+ TextDirectionHeuristic dir2 = dirs[(diri + 1) % dirs.length];
+
+ assertSameStaticLayout(
+ LOREM_IPSUM,
+ new LayoutParam(b1, f1, p1, dir1),
+ new LayoutParam(b2, f2, p2, dir2));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Config(minSdk = P) // TODO(hoisie): Fix in Android O/O_MR1
+ @Test
+ public void testReplacementFontMetricsTest() {
+ Context context = RuntimeEnvironment.getApplication();
+
+ Typeface tf = new Typeface.Builder(context.getAssets(), "fonts/samplefont.ttf").build();
+ assertNotNull(tf);
+ TextPaint paint = new TextPaint();
+ paint.setTypeface(tf);
+
+ ReplacementSpan firstReplacement = mock(ReplacementSpan.class);
+ ArgumentCaptor<FontMetricsInt> fm1Captor = ArgumentCaptor.forClass(FontMetricsInt.class);
+ when(firstReplacement.getSize(
+ any(Paint.class), any(CharSequence.class), anyInt(), anyInt(), fm1Captor.capture()))
+ .thenReturn(0);
+ TextAppearanceSpan firstStyleSpan =
+ new TextAppearanceSpan(
+ null /* family */,
+ Typeface.NORMAL /* style */,
+ 100 /* text size, 1em = 100px */,
+ null /* text color */,
+ null /* link color */);
+
+ ReplacementSpan secondReplacement = mock(ReplacementSpan.class);
+ ArgumentCaptor<FontMetricsInt> fm2Captor = ArgumentCaptor.forClass(FontMetricsInt.class);
+ when(secondReplacement.getSize(
+ any(Paint.class),
+ any(CharSequence.class),
+ any(Integer.class),
+ any(Integer.class),
+ fm2Captor.capture()))
+ .thenReturn(0);
+ TextAppearanceSpan secondStyleSpan =
+ new TextAppearanceSpan(
+ null /* family */,
+ Typeface.NORMAL /* style */,
+ 200 /* text size, 1em = 200px */,
+ null /* text color */,
+ null /* link color */);
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder("Hello, World\nHello, Android");
+ ssb.setSpan(firstStyleSpan, 0, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ ssb.setSpan(firstReplacement, 0, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ ssb.setSpan(secondStyleSpan, 13, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ ssb.setSpan(secondReplacement, 13, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ StaticLayout.Builder.obtain(ssb, 0, ssb.length(), paint, Integer.MAX_VALUE).build();
+
+ FontMetricsInt firstMetrics = fm1Captor.getValue();
+ FontMetricsInt secondMetrics = fm2Captor.getValue();
+
+ // The samplefont.ttf has 0.8em ascent and 0.2em descent.
+ assertEquals(-100, firstMetrics.ascent);
+ assertEquals(20, firstMetrics.descent);
+
+ assertEquals(-200, secondMetrics.ascent);
+ assertEquals(40, secondMetrics.descent);
+ }
+
+ @Config(minSdk = Q)
+ @Test
+ public void testChangeFontMetricsLineHeightBySpanTest() {
+ final TextPaint paint = new TextPaint();
+ paint.setTextSize(50);
+ final SpannableString spanStr0 = new SpannableString(LOREM_IPSUM);
+ // Make sure the final layout contain multiple lines.
+ final int width = (int) paint.measureText(spanStr0.toString()) / 5;
+ final int expectedHeight0 = 25;
+
+ spanStr0.setSpan(
+ new LineHeightSpan.Standard(expectedHeight0),
+ 0,
+ spanStr0.length(),
+ SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+ StaticLayout layout0 =
+ StaticLayout.Builder.obtain(spanStr0, 0, spanStr0.length(), paint, width).build();
+
+ // We need at least 3 lines for testing.
+ assertTrue(layout0.getLineCount() > 2);
+ // Omit the first and last line, because their line hight might be different due to
+ // padding.
+ for (int i = 1; i < layout0.getLineCount() - 1; ++i) {
+ assertEquals(expectedHeight0, layout0.getLineBottom(i) - layout0.getLineTop(i));
+ }
+
+ final SpannableString spanStr1 = new SpannableString(LOREM_IPSUM);
+ int expectedHeight1 = 100;
+
+ spanStr1.setSpan(
+ new LineHeightSpan.Standard(expectedHeight1),
+ 0,
+ spanStr1.length(),
+ SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+ StaticLayout layout1 =
+ StaticLayout.Builder.obtain(spanStr1, 0, spanStr1.length(), paint, width).build();
+
+ for (int i = 1; i < layout1.getLineCount() - 1; ++i) {
+ assertEquals(expectedHeight1, layout1.getLineBottom(i) - layout1.getLineTop(i));
+ }
+ }
+
+ @Config(minSdk = Q)
+ @Test
+ public void testChangeFontMetricsLineHeightBySpanMultipleTimesTest() {
+ final TextPaint paint = new TextPaint();
+ paint.setTextSize(50);
+ final SpannableString spanStr = new SpannableString(LOREM_IPSUM);
+ final int width = (int) paint.measureText(spanStr.toString()) / 5;
+ final int expectedHeight = 100;
+
+ spanStr.setSpan(
+ new LineHeightSpan.Standard(25),
+ 0,
+ spanStr.length(),
+ SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+ // Only the last span is effective.
+ spanStr.setSpan(
+ new LineHeightSpan.Standard(expectedHeight),
+ 0,
+ spanStr.length(),
+ SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+ StaticLayout layout =
+ StaticLayout.Builder.obtain(spanStr, 0, spanStr.length(), paint, width).build();
+
+ assertTrue(layout.getLineCount() > 2);
+ for (int i = 1; i < layout.getLineCount() - 1; ++i) {
+ assertEquals(expectedHeight, layout.getLineBottom(i) - layout.getLineTop(i));
+ }
+ }
+
+ private static class FakeLineBackgroundSpan implements LineBackgroundSpan {
+ // Whenever drawBackground() is called, the start and end of
+ // the line will be stored into history as an array in the
+ // format of [start, end].
+ private final List<int[]> history;
+
+ FakeLineBackgroundSpan() {
+ history = new ArrayList<int[]>();
+ }
+
+ @Override
+ public void drawBackground(
+ Canvas c,
+ Paint p,
+ int left,
+ int right,
+ int top,
+ int baseline,
+ int bottom,
+ CharSequence text,
+ int start,
+ int end,
+ int lnum) {
+ history.add(new int[] {start, end});
+ }
+
+ List<int[]> getHistory() {
+ return history;
+ }
+ }
+
+ private void testLineBackgroundSpanInRange(String text, int start, int end) {
+ final SpannableString spanStr = new SpannableString(text);
+ final FakeLineBackgroundSpan span = new FakeLineBackgroundSpan();
+ spanStr.setSpan(span, start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ final TextPaint paint = new TextPaint();
+ paint.setTextSize(50);
+ final int width = (int) paint.measureText(spanStr.toString()) / 5;
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(spanStr, 0, spanStr.length(), paint, width).build();
+
+ // One line is too simple, need more to test.
+ assertTrue(layout.getLineCount() > 1);
+ drawToBitmap(layout);
+ List<int[]> history = span.getHistory();
+
+ if (history.isEmpty()) {
+ // drawBackground() of FakeLineBackgroundSpan was never called.
+ // This only happens when the length of the span is zero.
+ assertTrue(start >= end);
+ return;
+ }
+
+ // Check if drawBackground() is corrected called for each affected line.
+ int lastLineEnd = history.get(0)[0];
+ for (int[] lineRange : history) {
+ // The range of line must intersect with the span.
+ assertTrue(lineRange[0] < end && lineRange[1] > start);
+ // Check:
+ // 1. drawBackground() is called in the correct sequence.
+ // 2. drawBackground() is called only once for each affected line.
+ assertEquals(lastLineEnd, lineRange[0]);
+ lastLineEnd = lineRange[1];
+ }
+
+ int[] firstLineRange = history.get(0);
+ int[] lastLineRange = history.get(history.size() - 1);
+
+ // Check if affected lines match the span coverage.
+ assertTrue(firstLineRange[0] <= start && end <= lastLineRange[1]);
+ }
+
+ @Test
+ public void testDrawWithLineBackgroundSpanCoverWholeText() {
+ testLineBackgroundSpanInRange(LOREM_IPSUM, 0, LOREM_IPSUM.length());
+ }
+
+ @Test
+ public void testDrawWithLineBackgroundSpanCoverNothing() {
+ int i = 0;
+ // Zero length Spans.
+ testLineBackgroundSpanInRange(LOREM_IPSUM, i, i);
+ i = LOREM_IPSUM.length() / 2;
+ testLineBackgroundSpanInRange(LOREM_IPSUM, i, i);
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testDrawWithLineBackgroundSpanCoverPart() {
+ int start = 0;
+ int end = LOREM_IPSUM.length() / 2;
+ testLineBackgroundSpanInRange(LOREM_IPSUM, start, end);
+
+ start = LOREM_IPSUM.length() / 2;
+ end = LOREM_IPSUM.length();
+ testLineBackgroundSpanInRange(LOREM_IPSUM, start, end);
+ }
+
+ @Config(minSdk = VERSION_CODES.R)
+ @Test
+ public void testBidiVisibleEnd() {
+ TextPaint paint = new TextPaint();
+ // The default text size is too small and not useful for handling line breaks.
+ // Make it bigger.
+ paint.setTextSize(32);
+
+ final String input = "\u05D0aaaaaa\u3000 aaaaaa";
+ // To make line break happen, pass slightly shorter width from the full text width.
+ final int lineBreakWidth = (int) (paint.measureText(input) * 0.8);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(input, 0, input.length(), paint, lineBreakWidth).build();
+
+ // Make sure getLineMax won't cause crashes.
+ // getLineMax eventually calls TextLine.measure which was the problematic method.
+ layout.getLineMax(0);
+
+ final Bitmap bmp =
+ Bitmap.createBitmap(layout.getWidth(), layout.getHeight(), Bitmap.Config.RGB_565);
+ final Canvas c = new Canvas(bmp);
+ // Make sure draw won't cause crashes.
+ // draw eventualy calls TextLine.draw which was the problematic method.
+ layout.draw(c);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeSumPathEffectTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeSumPathEffectTest.java
new file mode 100644
index 000000000..869552aa5
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeSumPathEffectTest.java
@@ -0,0 +1,62 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static org.junit.Assert.assertEquals;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.CornerPathEffect;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.PathEffect;
+import android.graphics.SumPathEffect;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeSumPathEffectTest {
+ private static final int WIDTH = 100;
+ private static final int HEIGHT = 100;
+
+ @Test
+ public void testSumPathEffect() {
+ Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.BLACK);
+ Bitmap expected = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
+ expected.eraseColor(Color.BLACK);
+
+ Path path = new Path();
+ path.addRect(10, 10, WIDTH - 10, HEIGHT - 10, Direction.CW);
+
+ PathEffect first = new CornerPathEffect(40);
+ Canvas canvas = new Canvas(expected);
+ Paint paint = new Paint();
+ paint.setColor(Color.GREEN);
+ paint.setPathEffect(first);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(0); // 1-pixel hairline
+ paint.setAntiAlias(false);
+ canvas.drawPath(path, paint);
+
+ PathEffect second = new DashPathEffect(new float[] {10, 5}, 5);
+ paint.setPathEffect(second);
+ canvas.drawPath(path, paint);
+
+ SumPathEffect sumPathEffect = new SumPathEffect(second, first);
+ paint.setPathEffect(sumPathEffect);
+ canvas = new Canvas(bitmap);
+ canvas.drawPath(path, paint);
+
+ for (int i = 0; i < WIDTH; i++) {
+ for (int j = 0; j < HEIGHT; j++) {
+ assertEquals(expected.getPixel(i, j), bitmap.getPixel(i, j));
+ }
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeSweepGradientTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeSweepGradientTest.java
new file mode 100644
index 000000000..bcb0891b9
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeSweepGradientTest.java
@@ -0,0 +1,387 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorSpace;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.util.Log;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeSweepGradientTest {
+ private static final int SIZE = 200;
+ private static final int CENTER = SIZE / 2;
+ private static final int RADIUS = 80;
+ private static final int NUM_STEPS = 100;
+ private static final int TOLERANCE = 10;
+
+ private Paint paint;
+ private Canvas canvas;
+ private Bitmap bitmap;
+
+ @Before
+ public void setup() {
+ paint = new Paint();
+ bitmap = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.TRANSPARENT);
+ canvas = new Canvas(bitmap);
+ }
+
+ @Test
+ public void test2Colors() {
+ final int[] colors = new int[] {Color.GREEN, Color.RED};
+ final float[] positions = new float[] {0f, 1f};
+ Shader shader = new SweepGradient(CENTER, CENTER, colors[0], colors[1]);
+ paint.setShader(shader);
+ canvas.drawRect(new Rect(0, 0, SIZE, SIZE), paint);
+ verifyColors(colors, positions, TOLERANCE);
+ }
+
+ @Test
+ public void testColorArray() {
+ final int[] colors = new int[] {Color.GREEN, Color.RED, Color.BLUE};
+ final float[] positions = new float[] {0f, 0.3f, 1f};
+ Shader shader = new SweepGradient(CENTER, CENTER, colors, positions);
+ paint.setShader(shader);
+ canvas.drawRect(new Rect(0, 0, SIZE, SIZE), paint);
+
+ verifyColors(colors, positions, TOLERANCE);
+ }
+
+ @Test
+ public void testMultiColor() {
+ final int[] colors = new int[] {Color.GREEN, Color.RED, Color.BLUE, Color.GREEN};
+ final float[] positions = new float[] {0f, 0.25f, 0.5f, 1f};
+
+ Shader shader = new SweepGradient(CENTER, CENTER, colors, positions);
+ paint.setShader(shader);
+ canvas.drawRect(new Rect(0, 0, SIZE, SIZE), paint);
+
+ verifyColors(colors, positions, TOLERANCE);
+ }
+
+ private void verifyColors(int[] colors, float[] positions, int tolerance) {
+ final double twoPi = Math.PI * 2;
+ final double step = twoPi / NUM_STEPS;
+
+ // exclude angle 0, which is not defined
+ for (double rad = step; rad <= twoPi - step; rad += step) {
+ int x = CENTER + (int) (Math.cos(rad) * RADIUS);
+ int y = CENTER + (int) (Math.sin(rad) * RADIUS);
+
+ float relPos = (float) (rad / twoPi);
+ int idx;
+ int color;
+ for (idx = 0; idx < positions.length; idx++) {
+ if (positions[idx] > relPos) {
+ break;
+ }
+ }
+ if (idx == 0) {
+ // use start color
+ color = colors[0];
+ } else if (idx == positions.length) {
+ // clamp to end color
+ color = colors[positions.length - 1];
+ } else {
+ // linear interpolation
+ int i1 = idx - 1; // index of next lower color and position
+ int i2 = idx; // index of next higher color and position
+ double delta = (relPos - positions[i1]) / (positions[i2] - positions[i1]);
+ int alpha =
+ (int) ((1d - delta) * Color.alpha(colors[i1]) + delta * Color.alpha(colors[i2]));
+ int red = (int) ((1d - delta) * Color.red(colors[i1]) + delta * Color.red(colors[i2]));
+ int green =
+ (int) ((1d - delta) * Color.green(colors[i1]) + delta * Color.green(colors[i2]));
+ int blue = (int) ((1d - delta) * Color.blue(colors[i1]) + delta * Color.blue(colors[i2]));
+ color = Color.argb(alpha, red, green, blue);
+ }
+
+ int pixel = bitmap.getPixel(x, y);
+
+ try {
+ assertEquals(Color.alpha(color), Color.alpha(pixel), tolerance);
+ assertEquals(Color.red(color), Color.red(pixel), tolerance);
+ assertEquals(Color.green(color), Color.green(pixel), tolerance);
+ assertEquals(Color.blue(color), Color.blue(pixel), tolerance);
+ } catch (Error e) {
+ Log.w(
+ getClass().getName(),
+ "rad="
+ + rad
+ + ", x="
+ + x
+ + ", y="
+ + y
+ + "pixel="
+ + Integer.toHexString(pixel)
+ + ", color="
+ + Integer.toHexString(color));
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ public void testZeroScaleMatrix() {
+ SweepGradient gradient =
+ new SweepGradient(1, 0.5f, new int[] {Color.BLUE, Color.RED, Color.BLUE}, null);
+ Matrix m = new Matrix();
+ m.setScale(0, 0);
+ gradient.setLocalMatrix(m);
+
+ Bitmap bitmap = Bitmap.createBitmap(2, 1, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.BLACK);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setShader(gradient);
+ canvas.drawPaint(paint);
+
+ // red to left, blue to right
+ ColorUtils.verifyColor(Color.BLACK, bitmap.getPixel(0, 0), 1);
+ ColorUtils.verifyColor(Color.BLACK, bitmap.getPixel(1, 0), 1);
+ }
+
+ @Test
+ public void testNullColorInts() {
+ int[] colors = null;
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, colors, null);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testNullColorLongs() {
+ long[] colors = null;
+ assertThrows(
+ NullPointerException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, colors, null);
+ });
+ }
+
+ @Test
+ public void testNoColorInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, new int[0], null);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testNoColorLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, new long[0], null);
+ });
+ }
+
+ @Test
+ public void testOneColorInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, new int[1], null);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testOneColorLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, new long[1], null);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchColorLongs() {
+ long[] colors = new long[2];
+ colors[0] = Color.pack(Color.BLUE);
+ colors[1] = Color.pack(.5f, .5f, .5f, 1.0f, ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, colors, null);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchColorLongs2() {
+ long color0 = Color.pack(Color.BLUE);
+ long color1 = Color.pack(.5f, .5f, .5f, 1.0f, ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, color0, color1);
+ });
+ }
+
+ @Test
+ public void testMismatchPositionsInts() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, new int[2], new float[3]);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testMismatchPositionsLongs() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, new long[2], new float[3]);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLongs() {
+ long[] colors = new long[2];
+ colors[0] = -1L;
+ colors[0] = -2L;
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, colors, null);
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLong() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, -1L, Color.pack(Color.RED));
+ });
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testInvalidColorLong2() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradient unused = new SweepGradient(1, 0.5f, Color.pack(Color.RED), -1L);
+ });
+ }
+
+ private String toString(double angle) {
+ return String.format("%.2f", angle) + "(pi)";
+ }
+
+ @Test
+ @Config(minSdk = Q)
+ public void testColorLong() {
+ ColorSpace p3 = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
+ long red = Color.pack(1, 0, 0, 1, p3);
+ long blue = Color.pack(0, 0, 1, 1, p3);
+ SweepGradient gradient = new SweepGradient(50, 50, red, blue);
+
+ Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.RGBA_F16);
+ bitmap.eraseColor(Color.TRANSPARENT);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setShader(gradient);
+ canvas.drawPaint(paint);
+
+ final ColorSpace bitmapColorSpace = bitmap.getColorSpace();
+ Function<Long, Color> convert =
+ (l) -> {
+ return Color.valueOf(Color.convert(l, bitmapColorSpace));
+ };
+
+ Color lastColor = null;
+ double lastAngle = 0;
+ for (double angle = Math.PI / 8.0; angle < Math.PI * 2.0; angle += Math.PI / 8.0) {
+ // currentColor is the Color at this angle.
+ Color currentColor = null;
+ double lastRadius = 0;
+ for (double radius = 4; radius < 25; radius += 4) {
+ double dx = Math.cos(angle) * radius;
+ double dy = Math.sin(angle) * radius;
+ int x = 50 + (int) dx;
+ int y = 50 + (int) dy;
+ Color c = bitmap.getColor(x, y);
+ if (currentColor == null) {
+ // Checking the first radius at this angle.
+ currentColor = c;
+ if (lastColor == null) {
+ // This should be pretty close to the initial color.
+ ColorUtils.verifyColor(
+ "First color (at angle "
+ + toString(angle)
+ + " and radius "
+ + radius
+ + " should be mostly red",
+ convert.apply(red),
+ c,
+ .08f);
+ lastColor = currentColor;
+ lastAngle = angle;
+ } else {
+ assertTrue(
+ "Angle "
+ + toString(angle)
+ + " should be less red than prior angle "
+ + toString(lastAngle),
+ c.red() < lastColor.red());
+ assertTrue(
+ "Angle "
+ + toString(angle)
+ + " should be more blue than prior angle "
+ + toString(lastAngle),
+ c.blue() > lastColor.blue());
+ }
+ } else {
+ // Already have a Color at this angle. This one should match.
+ ColorUtils.verifyColor(
+ "Radius "
+ + radius
+ + " at angle "
+ + toString(angle)
+ + " should match same angle with radius "
+ + lastRadius,
+ currentColor,
+ c,
+ .05f);
+ }
+ lastRadius = radius;
+ }
+
+ lastColor = currentColor;
+ lastAngle = angle;
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTableMaskFilterTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTableMaskFilterTest.java
new file mode 100644
index 000000000..d0ef0a786
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTableMaskFilterTest.java
@@ -0,0 +1,46 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.TableMaskFilter;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeTableMaskFilterTest {
+ private static final int TOLERANCE = 2;
+
+ private void verifyColor(int expected, int actual) {
+ ColorUtils.verifyColor(expected, actual, TOLERANCE);
+ }
+
+ @Test
+ public void testConstructor() {
+ Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+
+ paint.setColor(Color.MAGENTA);
+ paint.setMaskFilter(TableMaskFilter.CreateGammaTable(10.0f));
+ canvas.drawPaint(paint);
+ verifyColor(Color.MAGENTA, bitmap.getPixel(0, 0));
+
+ paint.setColor(Color.MAGENTA);
+ paint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 20));
+ canvas.drawPaint(paint);
+ verifyColor(Color.MAGENTA, bitmap.getPixel(0, 0));
+
+ paint.setColor(Color.MAGENTA);
+ paint.setMaskFilter(new TableMaskFilter(new byte[256]));
+ canvas.drawPaint(paint);
+ verifyColor(Color.MAGENTA, bitmap.getPixel(0, 0));
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeThreadedRendererTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeThreadedRendererTest.java
new file mode 100644
index 000000000..0a13f2c7f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeThreadedRendererTest.java
@@ -0,0 +1,21 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.P;
+
+import android.view.ThreadedRenderer;
+import androidx.test.core.app.ApplicationProvider;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = O, maxSdk = P)
+public class ShadowNativeThreadedRendererTest {
+ @Test
+ public void testInitialization() {
+ ThreadedRenderer unused =
+ ThreadedRenderer.create(ApplicationProvider.getApplicationContext(), false, "Name");
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTypefaceTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTypefaceTest.java
new file mode 100644
index 000000000..8497c6dfc
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeTypefaceTest.java
@@ -0,0 +1,354 @@
+package org.robolectric.integrationtests.nativegraphics;
+
+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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Locale;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(minSdk = O)
+@RunWith(RobolectricTestRunner.class)
+public class ShadowNativeTypefaceTest {
+ // generic family name for monospaced fonts
+ private static final String MONO = "monospace";
+ private static final String DEFAULT = null;
+ private static final String INVALID = "invalid-family-name";
+
+ private static float measureText(String text, Typeface typeface) {
+ final Paint paint = new Paint();
+ // Fix the locale so that fix the locale based fallback.
+ paint.setTextLocale(Locale.US);
+ paint.setTypeface(typeface);
+ return paint.measureText(text);
+ }
+
+ // list of family names to try when attempting to find a typeface with a given style
+ private static final String[] FAMILIES = {
+ null, "monospace", "serif", "sans-serif", "cursive", "arial", "times"
+ };
+
+ private final Context context = RuntimeEnvironment.getApplication();
+
+ /**
+ * Create a typeface of the given style. If the default font does not support the style, a number
+ * of generic families are tried.
+ *
+ * @return The typeface or null, if no typeface with the given style can be found.
+ */
+ private static Typeface createTypeface(int style) {
+ for (String family : FAMILIES) {
+ Typeface tf = Typeface.create(family, style);
+ if (tf.getStyle() == style) {
+ return tf;
+ }
+ }
+ return null;
+ }
+
+ @Test
+ public void typeface_notDefault() {
+ Typeface typeface1 =
+ Typeface.createFromAsset(context.getAssets(), "fonts/others/samplefont.ttf");
+ assertThat(typeface1).isNotEqualTo(Typeface.DEFAULT);
+ }
+
+ @Test
+ public void testIsBold() {
+ Typeface typeface = createTypeface(Typeface.BOLD);
+ if (typeface != null) {
+ assertEquals(Typeface.BOLD, typeface.getStyle());
+ assertTrue(typeface.isBold());
+ assertFalse(typeface.isItalic());
+ }
+
+ typeface = createTypeface(Typeface.ITALIC);
+ if (typeface != null) {
+ assertEquals(Typeface.ITALIC, typeface.getStyle());
+ assertFalse(typeface.isBold());
+ assertTrue(typeface.isItalic());
+ }
+
+ typeface = createTypeface(Typeface.BOLD_ITALIC);
+ if (typeface != null) {
+ assertEquals(Typeface.BOLD_ITALIC, typeface.getStyle());
+ assertTrue(typeface.isBold());
+ assertTrue(typeface.isItalic());
+ }
+
+ typeface = createTypeface(Typeface.NORMAL);
+ if (typeface != null) {
+ assertEquals(Typeface.NORMAL, typeface.getStyle());
+ assertFalse(typeface.isBold());
+ assertFalse(typeface.isItalic());
+ }
+ }
+
+ @Test
+ public void testCreate() {
+ Typeface typeface = Typeface.create(DEFAULT, Typeface.NORMAL);
+ assertNotNull(typeface);
+ typeface = Typeface.create(MONO, Typeface.BOLD);
+ assertNotNull(typeface);
+ typeface = Typeface.create(INVALID, Typeface.ITALIC);
+ assertNotNull(typeface);
+
+ typeface = Typeface.create(typeface, Typeface.NORMAL);
+ assertNotNull(typeface);
+ typeface = Typeface.create(typeface, Typeface.BOLD);
+ assertNotNull(typeface);
+ }
+
+ @Test
+ public void testDefaultFromStyle() {
+ Typeface typeface = Typeface.defaultFromStyle(Typeface.NORMAL);
+ assertNotNull(typeface);
+ typeface = Typeface.defaultFromStyle(Typeface.BOLD);
+ assertNotNull(typeface);
+ typeface = Typeface.defaultFromStyle(Typeface.ITALIC);
+ assertNotNull(typeface);
+ typeface = Typeface.defaultFromStyle(Typeface.BOLD_ITALIC);
+ assertNotNull(typeface);
+ }
+
+ @Test
+ public void testConstants() {
+ assertNotNull(Typeface.DEFAULT);
+ assertNotNull(Typeface.DEFAULT_BOLD);
+ assertNotNull(Typeface.MONOSPACE);
+ assertNotNull(Typeface.SANS_SERIF);
+ assertNotNull(Typeface.SERIF);
+ }
+
+ @Test
+ public void testCreateFromAssetNull() {
+ // input abnormal params.
+ assertThrows(NullPointerException.class, () -> Typeface.createFromAsset(null, null));
+ }
+
+ @Test
+ public void testCreateFromAssetNullPath() {
+ // input abnormal params.
+ assertThrows(
+ NullPointerException.class, () -> Typeface.createFromAsset(context.getAssets(), null));
+ }
+
+ @Test
+ public void testCreateFromAssetInvalidPath() {
+ // input abnormal params.
+ assertThrows(
+ RuntimeException.class,
+ () -> Typeface.createFromAsset(context.getAssets(), "invalid path"));
+ }
+
+ @Test
+ public void testCreateFromAsset() {
+ Typeface typeface =
+ Typeface.createFromAsset(context.getAssets(), "fonts/others/samplefont.ttf");
+ assertNotNull(typeface);
+ }
+
+ @Test
+ public void testCreateFromFileByFileReferenceNull() {
+ // input abnormal params.
+ assertThrows(NullPointerException.class, () -> Typeface.createFromFile((File) null));
+ }
+
+ @Test
+ public void testCreateFromFileByFileReference() throws IOException {
+ File file = new File(obtainPath());
+ Typeface typeface = Typeface.createFromFile(file);
+ assertNotNull(typeface);
+ }
+
+ @Test
+ public void testCreateFromFileWithInvalidPath() throws IOException {
+ File file = new File("/invalid/path");
+ assertThrows(RuntimeException.class, () -> Typeface.createFromFile(file));
+ }
+
+ @Test
+ public void testCreateFromFileByFileNameNull() throws IOException {
+ // input abnormal params.
+ assertThrows(NullPointerException.class, () -> Typeface.createFromFile((String) null));
+ }
+
+ @Test
+ public void testCreateFromFileByInvalidFileName() throws IOException {
+ // input abnormal params.
+ assertThrows(RuntimeException.class, () -> Typeface.createFromFile("/invalid/path"));
+ }
+
+ @Test
+ public void testCreateFromFileByFileName() throws IOException {
+ Typeface typeface = Typeface.createFromFile(obtainPath());
+ assertNotNull(typeface);
+ }
+
+ private String obtainPath() throws IOException {
+ File dir = context.getFilesDir();
+ dir.mkdirs();
+ File file = new File(dir, "test.jpg");
+ if (!file.createNewFile()) {
+ if (!file.exists()) {
+ fail("Failed to create new File!");
+ }
+ }
+ InputStream is = context.getAssets().open("fonts/others/samplefont.ttf");
+ FileOutputStream fOutput = new FileOutputStream(file);
+ ByteStreams.copy(is, fOutput);
+ is.close();
+ fOutput.close();
+ return file.getPath();
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void testInvalidCmapFont() {
+ Typeface typeface =
+ Typeface.createFromAsset(context.getAssets(), "fonts/security/bombfont.ttf");
+ assertNotNull(typeface);
+ final String testString = "abcde";
+ float widthDefaultTypeface = measureText(testString, Typeface.DEFAULT);
+ float widthCustomTypeface = measureText(testString, typeface);
+ assertEquals(widthDefaultTypeface, widthCustomTypeface, 1.0f);
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void testInvalidCmapFont2() {
+ Typeface typeface =
+ Typeface.createFromAsset(context.getAssets(), "fonts/security/bombfont2.ttf");
+ assertNotNull(typeface);
+ final String testString = "abcde";
+ float widthDefaultTypeface = measureText(testString, Typeface.DEFAULT);
+ float widthCustomTypeface = measureText(testString, typeface);
+ assertEquals(widthDefaultTypeface, widthCustomTypeface, 1.0f);
+ }
+
+ @Test
+ @Config(minSdk = O, maxSdk = O_MR1)
+ public void testInvalidCmapFont_o_omr1() {
+ Typeface typeface =
+ Typeface.createFromAsset(context.getAssets(), "fonts/security/bombfont.ttf");
+ assertNotNull(typeface);
+ Paint p = new Paint();
+ final String testString = "abcde";
+ float widthDefaultTypeface = p.measureText(testString);
+ p.setTypeface(typeface);
+ float widthCustomTypeface = p.measureText(testString);
+ assertEquals(widthDefaultTypeface, widthCustomTypeface, 1.0f);
+ }
+
+ @Test
+ @Config(minSdk = O, maxSdk = O_MR1)
+ public void testInvalidCmapFont2_o_omr1() {
+ Typeface typeface =
+ Typeface.createFromAsset(context.getAssets(), "fonts/security/bombfont2.ttf");
+ assertNotNull(typeface);
+ Paint p = new Paint();
+ final String testString = "abcde";
+ float widthDefaultTypeface = p.measureText(testString);
+ p.setTypeface(typeface);
+ float widthCustomTypeface = p.measureText(testString);
+ assertEquals(widthDefaultTypeface, widthCustomTypeface, 1.0f);
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void testInvalidCmapFont_tooLargeCodePoints() {
+ // Following three font doen't have any coverage between U+0000..U+10FFFF. Just make sure
+ // they don't crash us.
+ final String[] invalidCMAPFonts = {
+ "fonts/security/out_of_unicode_start_cmap12.ttf",
+ "fonts/security/out_of_unicode_end_cmap12.ttf",
+ "fonts/security/too_large_start_cmap12.ttf",
+ "fonts/security/too_large_end_cmap12.ttf",
+ };
+ for (final String file : invalidCMAPFonts) {
+ final Typeface typeface = Typeface.createFromAsset(context.getAssets(), file);
+ assertNotNull(typeface);
+ }
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void testInvalidCmapFont_unsortedEntries() {
+ // Following two font files have glyph for U+0400 and U+0100 but the fonts must not be used
+ // due to invalid cmap data. For more details, see each ttx source file.
+ final String[] invalidCMAPFonts = {
+ "fonts/security/unsorted_cmap4.ttf", "fonts/security/unsorted_cmap12.ttf"
+ };
+ for (final String file : invalidCMAPFonts) {
+ final Typeface typeface = Typeface.createFromAsset(context.getAssets(), file);
+ assertNotNull(typeface);
+ final String testString = "\u0100\u0400";
+ final float widthDefaultTypeface = measureText(testString, Typeface.DEFAULT);
+ final float widthCustomTypeface = measureText(testString, typeface);
+ assertEquals(widthDefaultTypeface, widthCustomTypeface, 0.0f);
+ }
+
+ // Following two font files have glyph for U+0400 U+FE00 and U+0100 U+FE00 but the fonts
+ // must not be used due to invalid cmap data. For more details, see each ttx source file.
+ final String[] invalidCMAPVSFonts = {
+ "fonts/security/unsorted_cmap14_default_uvs.ttf",
+ "fonts/security/unsorted_cmap14_non_default_uvs.ttf"
+ };
+ for (final String file : invalidCMAPVSFonts) {
+ final Typeface typeface = Typeface.createFromAsset(context.getAssets(), file);
+ assertNotNull(typeface);
+ final String testString = "\u0100\uFE00\u0400\uFE00";
+ final float widthDefaultTypeface = measureText(testString, Typeface.DEFAULT);
+ final float widthCustomTypeface = measureText(testString, typeface);
+ assertEquals(widthDefaultTypeface, widthCustomTypeface, 0.0f);
+ }
+ }
+
+ @Test
+ @Config(sdk = P)
+ public void testCreateFromAsset_cachesTypeface() {
+ Typeface typeface1 =
+ Typeface.createFromAsset(context.getAssets(), "fonts/others/samplefont.ttf");
+ assertNotNull(typeface1);
+
+ Typeface typeface2 =
+ Typeface.createFromAsset(context.getAssets(), "fonts/others/samplefont.ttf");
+ assertNotNull(typeface2);
+ assertSame("Same font asset should return same Typeface object", typeface1, typeface2);
+
+ Typeface typeface3 =
+ Typeface.createFromAsset(context.getAssets(), "fonts/others/samplefont2.ttf");
+ assertNotNull(typeface3);
+ assertNotSame(
+ "Different font asset should return different Typeface object", typeface2, typeface3);
+
+ Typeface typeface4 =
+ Typeface.createFromAsset(context.getAssets(), "fonts/others/samplefont3.ttf");
+ assertNotNull(typeface4);
+ assertNotSame(
+ "Different font asset should return different Typeface object", typeface2, typeface4);
+ assertNotSame(
+ "Different font asset should return different Typeface object", typeface3, typeface4);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeVectorDrawableTest.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeVectorDrawableTest.java
new file mode 100644
index 000000000..7ffe54f13
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/ShadowNativeVectorDrawableTest.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics;
+
+import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.Q;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.XmlResourceParser;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Drawable.ConstantState;
+import android.graphics.drawable.VectorDrawable;
+import android.util.AttributeSet;
+import android.util.Xml;
+import androidx.annotation.Nullable;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowDrawable;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = O)
+public class ShadowNativeVectorDrawableTest {
+
+ // Separate the test assets into different groups such that we could isolate the issue faster.
+ // Some new APIs or bug fixes only exist in particular os version, such that we name the tests
+ // and associated assets with OS code name L, M, N etc...
+ private static final int[] BASIC_ICON_RES_IDS =
+ new int[] {
+ R.drawable.vector_icon_create,
+ R.drawable.vector_icon_delete,
+ R.drawable.vector_icon_heart,
+ R.drawable.vector_icon_schedule,
+ R.drawable.vector_icon_settings,
+ R.drawable.vector_icon_random_path_1,
+ R.drawable.vector_icon_random_path_2,
+ R.drawable.vector_icon_repeated_cq,
+ R.drawable.vector_icon_repeated_st,
+ R.drawable.vector_icon_repeated_a_1,
+ R.drawable.vector_icon_repeated_a_2,
+ R.drawable.vector_icon_clip_path_1,
+ };
+
+ private static final int[] BASIC_GOLDEN_IMAGES =
+ new int[] {
+ R.drawable.vector_icon_create_golden,
+ R.drawable.vector_icon_delete_golden,
+ R.drawable.vector_icon_heart_golden,
+ R.drawable.vector_icon_schedule_golden,
+ R.drawable.vector_icon_settings_golden,
+ R.drawable.vector_icon_random_path_1_golden,
+ R.drawable.vector_icon_random_path_2_golden,
+ R.drawable.vector_icon_repeated_cq_golden,
+ R.drawable.vector_icon_repeated_st_golden,
+ R.drawable.vector_icon_repeated_a_1_golden,
+ R.drawable.vector_icon_repeated_a_2_golden,
+ R.drawable.vector_icon_clip_path_1_golden,
+ };
+
+ private static final int[] L_M_ICON_RES_IDS =
+ new int[] {
+ R.drawable.vector_icon_transformation_1,
+ R.drawable.vector_icon_transformation_2,
+ R.drawable.vector_icon_transformation_3,
+ R.drawable.vector_icon_transformation_4,
+ R.drawable.vector_icon_transformation_5,
+ R.drawable.vector_icon_transformation_6,
+ R.drawable.vector_icon_render_order_1,
+ R.drawable.vector_icon_render_order_2,
+ R.drawable.vector_icon_stroke_1,
+ R.drawable.vector_icon_stroke_2,
+ R.drawable.vector_icon_stroke_3,
+ R.drawable.vector_icon_scale_1,
+ R.drawable.vector_icon_scale_2,
+ R.drawable.vector_icon_scale_3,
+ R.drawable.vector_icon_group_clip,
+ };
+
+ private static final int[] L_M_GOLDEN_IMAGES =
+ new int[] {
+ R.drawable.vector_icon_transformation_1_golden,
+ R.drawable.vector_icon_transformation_2_golden,
+ R.drawable.vector_icon_transformation_3_golden,
+ R.drawable.vector_icon_transformation_4_golden,
+ R.drawable.vector_icon_transformation_5_golden,
+ R.drawable.vector_icon_transformation_6_golden,
+ R.drawable.vector_icon_render_order_1_golden,
+ R.drawable.vector_icon_render_order_2_golden,
+ R.drawable.vector_icon_stroke_1_golden,
+ R.drawable.vector_icon_stroke_2_golden,
+ R.drawable.vector_icon_stroke_3_golden,
+ R.drawable.vector_icon_scale_1_golden,
+ R.drawable.vector_icon_scale_2_golden,
+ R.drawable.vector_icon_scale_3_golden,
+ R.drawable.vector_icon_group_clip_golden,
+ };
+
+ private static final int[] N_ICON_RES_IDS =
+ new int[] {
+ R.drawable.vector_icon_implicit_lineto,
+ R.drawable.vector_icon_arcto,
+ R.drawable.vector_icon_filltype_nonzero,
+ R.drawable.vector_icon_filltype_evenodd,
+ };
+
+ private static final int[] N_GOLDEN_IMAGES =
+ new int[] {
+ R.drawable.vector_icon_implicit_lineto_golden,
+ R.drawable.vector_icon_arcto_golden,
+ R.drawable.vector_icon_filltype_nonzero_golden,
+ R.drawable.vector_icon_filltype_evenodd_golden,
+ };
+
+ private static final int[] GRADIENT_ICON_RES_IDS =
+ new int[] {
+ R.drawable.vector_icon_gradient_1,
+ R.drawable.vector_icon_gradient_2,
+ R.drawable.vector_icon_gradient_3,
+ R.drawable.vector_icon_gradient_1_clamp,
+ R.drawable.vector_icon_gradient_2_repeat,
+ R.drawable.vector_icon_gradient_3_mirror,
+ };
+
+ private static final int[] GRADIENT_GOLDEN_IMAGES =
+ new int[] {
+ R.drawable.vector_icon_gradient_1_golden,
+ R.drawable.vector_icon_gradient_2_golden,
+ R.drawable.vector_icon_gradient_3_golden,
+ R.drawable.vector_icon_gradient_1_clamp_golden,
+ R.drawable.vector_icon_gradient_2_repeat_golden,
+ R.drawable.vector_icon_gradient_3_mirror_golden,
+ };
+
+ private static final int[] STATEFUL_RES_IDS =
+ new int[] {
+ // All these icons are using the same color state list, make sure it works for either
+ // the same drawable ID or different ID but same content.
+ R.drawable.vector_icon_state_list,
+ R.drawable.vector_icon_state_list,
+ R.drawable.vector_icon_state_list_2,
+ };
+
+ private static final int[][] STATEFUL_GOLDEN_IMAGES =
+ new int[][] {
+ {
+ R.drawable.vector_icon_state_list_golden,
+ R.drawable.vector_icon_state_list_golden,
+ R.drawable.vector_icon_state_list_2_golden
+ },
+ {
+ R.drawable.vector_icon_state_list_pressed_golden,
+ R.drawable.vector_icon_state_list_pressed_golden,
+ R.drawable.vector_icon_state_list_2_pressed_golden
+ }
+ };
+
+ private static final int[][] STATEFUL_STATE_SETS =
+ new int[][] {{}, {android.R.attr.state_pressed}};
+
+ private static final int IMAGE_WIDTH = 64;
+ private static final int IMAGE_HEIGHT = 64;
+
+ private static final boolean DBG_DUMP_PNG = false;
+
+ private Resources resources;
+ private Bitmap bitmap;
+ private Canvas canvas;
+ private Context context;
+
+ @Before
+ public void setup() {
+ final int width = IMAGE_WIDTH;
+ final int height = IMAGE_HEIGHT;
+
+ // Older API levels create an immutable bitmap from this function, despite newer API levels
+ // creating a mutable one. So, we copy it into a mutable bitmap.
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ assertTrue("Expected bitmap to be mutable", bitmap.isMutable());
+
+ canvas = new Canvas(bitmap);
+ context = ApplicationProvider.getApplicationContext();
+ resources = context.getResources();
+ }
+
+ @Test
+ public void testBasicVectorDrawables() throws XmlPullParserException, IOException {
+ verifyVectorDrawables(BASIC_ICON_RES_IDS, BASIC_GOLDEN_IMAGES, null);
+ }
+
+ @Test
+ public void testLMVectorDrawables() throws XmlPullParserException, IOException {
+ verifyVectorDrawables(L_M_ICON_RES_IDS, L_M_GOLDEN_IMAGES, null);
+ }
+
+ @Test
+ public void testNVectorDrawables() throws XmlPullParserException, IOException {
+ verifyVectorDrawables(N_ICON_RES_IDS, N_GOLDEN_IMAGES, null);
+ }
+
+ @Test
+ public void testVectorDrawableGradient() throws XmlPullParserException, IOException {
+ verifyVectorDrawables(GRADIENT_ICON_RES_IDS, GRADIENT_GOLDEN_IMAGES, null);
+ }
+
+ @Test
+ public void testColorStateList() throws XmlPullParserException, IOException {
+ for (int i = 0; i < STATEFUL_STATE_SETS.length; i++) {
+ verifyVectorDrawables(STATEFUL_RES_IDS, STATEFUL_GOLDEN_IMAGES[i], STATEFUL_STATE_SETS[i]);
+ }
+ }
+
+ private void verifyVectorDrawables(int[] resIds, int[] goldenImages, int[] stateSet)
+ throws XmlPullParserException, IOException {
+ for (int i = 0; i < resIds.length; i++) {
+ VectorDrawable vectorDrawable = new VectorDrawable();
+ vectorDrawable.setBounds(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
+
+ // Setup VectorDrawable from xml file and draw into the bitmap.
+ XmlPullParser parser = resources.getXml(resIds[i]);
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+
+ Theme theme = resources.newTheme();
+ theme.applyStyle(R.style.Theme_ThemedDrawableTest, true);
+ vectorDrawable.inflate(resources, parser, attrs, theme);
+
+ if (stateSet != null) {
+ vectorDrawable.setState(stateSet);
+ }
+
+ bitmap.eraseColor(0);
+ vectorDrawable.draw(canvas);
+
+ if (DBG_DUMP_PNG) {
+ String stateSetTitle = getTitleForStateSet(stateSet);
+ DrawableTestUtils.saveAutoNamedVectorDrawableIntoPNG(
+ context, bitmap, resIds[i], stateSetTitle);
+ } else {
+ // Start to compare
+ Bitmap golden = BitmapFactory.decodeResource(resources, goldenImages[i]);
+ DrawableTestUtils.compareImages(
+ resources.getString(resIds[i]),
+ bitmap,
+ golden,
+ DrawableTestUtils.PIXEL_ERROR_THRESHOLD,
+ DrawableTestUtils.PIXEL_ERROR_COUNT_THRESHOLD,
+ DrawableTestUtils.PIXEL_ERROR_TOLERANCE);
+ }
+ }
+ }
+
+ /**
+ * Generates an underline-delimited list of states in a given state set.
+ *
+ * <p>For example, the array {@code {R.attr.state_pressed}} would return {@code "pressed"}.
+ *
+ * @param stateSet a state set
+ * @return a string representing the state set, or {@code null} if the state set is empty or
+ * {@code null}
+ */
+ @Nullable
+ private String getTitleForStateSet(int[] stateSet) {
+ if (stateSet == null || stateSet.length == 0) {
+ return null;
+ }
+
+ final StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < stateSet.length; i++) {
+ final String state = resources.getResourceName(stateSet[i]);
+ final int stateIndex = state.indexOf("state_");
+ if (stateIndex >= 0) {
+ builder.append(state.substring(stateIndex + 6));
+ } else {
+ builder.append(stateSet[i]);
+ }
+ }
+
+ return builder.toString();
+ }
+
+ @Test
+ public void testGetChangingConfigurations() {
+ VectorDrawable vectorDrawable = new VectorDrawable();
+ ConstantState constantState = vectorDrawable.getConstantState();
+
+ // default
+ assertEquals(0, constantState.getChangingConfigurations());
+ assertEquals(0, vectorDrawable.getChangingConfigurations());
+
+ // change the drawable's configuration does not affect the state's configuration
+ vectorDrawable.setChangingConfigurations(0xff);
+ assertEquals(0xff, vectorDrawable.getChangingConfigurations());
+ assertEquals(0, constantState.getChangingConfigurations());
+
+ // the state's configuration get refreshed
+ constantState = vectorDrawable.getConstantState();
+ assertEquals(0xff, constantState.getChangingConfigurations());
+
+ // set a new configuration to drawable
+ vectorDrawable.setChangingConfigurations(0xff00);
+ assertEquals(0xff, constantState.getChangingConfigurations());
+ assertEquals(0xffff, vectorDrawable.getChangingConfigurations());
+ }
+
+ @Test
+ public void testGetConstantState() {
+ VectorDrawable vectorDrawable = new VectorDrawable();
+ ConstantState constantState = vectorDrawable.getConstantState();
+ assertNotNull(constantState);
+ assertEquals(0, constantState.getChangingConfigurations());
+
+ vectorDrawable.setChangingConfigurations(1);
+ constantState = vectorDrawable.getConstantState();
+ assertNotNull(constantState);
+ assertEquals(1, constantState.getChangingConfigurations());
+ }
+
+ @Test
+ public void testMutate() {
+ // d1 and d2 will be mutated, while d3 will not.
+ VectorDrawable d1 = (VectorDrawable) resources.getDrawable(R.drawable.vector_icon_create);
+ VectorDrawable d2 = (VectorDrawable) resources.getDrawable(R.drawable.vector_icon_create);
+ VectorDrawable d3 = (VectorDrawable) resources.getDrawable(R.drawable.vector_icon_create);
+ final int initialAlpha = d1.getAlpha();
+
+ d1.mutate();
+ d1.setAlpha(0x40);
+ assertEquals(0x40, d1.getAlpha());
+ assertEquals(initialAlpha, d2.getAlpha());
+ assertEquals(initialAlpha, d3.getAlpha());
+
+ d2.mutate();
+ d2.setAlpha(0x20);
+ assertEquals(0x40, d1.getAlpha());
+ assertEquals(0x20, d2.getAlpha());
+ assertEquals(initialAlpha, d3.getAlpha());
+ }
+
+ @Test
+ public void testMutatePreservesState() {
+ VectorDrawable d = (VectorDrawable) resources.getDrawable(R.drawable.vector_icon_create);
+ final int restoreAlpha = d.getAlpha();
+ try {
+ assertNotEquals(0x00, d.getAlpha());
+ d.setAlpha(0x00);
+ d.mutate();
+ // Test that after mutating, the alpha value is copied over.
+ assertEquals(0x00, d.getAlpha());
+ } finally {
+ // Restore the original drawable's alpha
+ resources.getDrawable(R.drawable.vector_icon_create).setAlpha(restoreAlpha);
+ }
+ }
+
+ @Test
+ public void testColorFilter() {
+ PorterDuffColorFilter filter = new PorterDuffColorFilter(Color.RED, Mode.SRC_IN);
+ VectorDrawable vectorDrawable = new VectorDrawable();
+ vectorDrawable.setColorFilter(filter);
+
+ assertEquals(filter, vectorDrawable.getColorFilter());
+ }
+
+ @Test
+ public void testGetOpacity() throws XmlPullParserException, IOException {
+ VectorDrawable vectorDrawable = new VectorDrawable();
+
+ assertEquals("Default alpha should be 255", 255, vectorDrawable.getAlpha());
+ assertEquals(
+ "Default opacity should be TRANSLUCENT",
+ PixelFormat.TRANSLUCENT,
+ vectorDrawable.getOpacity());
+
+ vectorDrawable.setAlpha(0);
+ assertEquals("Alpha should be 0 now", 0, vectorDrawable.getAlpha());
+ assertEquals(
+ "Opacity should be TRANSPARENT now", PixelFormat.TRANSPARENT, vectorDrawable.getOpacity());
+ }
+
+ @Test
+ public void testPreloadDensity() throws XmlPullParserException, IOException {
+ final int densityDpi = resources.getConfiguration().densityDpi;
+ try {
+ DrawableTestUtils.setResourcesDensity(resources, densityDpi);
+ verifyPreloadDensityInner(resources, densityDpi);
+ } finally {
+ DrawableTestUtils.setResourcesDensity(resources, densityDpi);
+ }
+ }
+
+ @Test
+ public void testPreloadDensity_tvdpi() throws XmlPullParserException, IOException {
+ final int densityDpi = resources.getConfiguration().densityDpi;
+ try {
+ DrawableTestUtils.setResourcesDensity(resources, 213);
+ verifyPreloadDensityInner(resources, 213);
+ } finally {
+ DrawableTestUtils.setResourcesDensity(resources, densityDpi);
+ }
+ }
+
+ @Test
+ @Config(minSdk = Q) // This test did not exist in API O-P
+ public void testOpticalInsets() {
+ VectorDrawable drawable = (VectorDrawable) resources.getDrawable(R.drawable.vector_icon_create);
+ assertEquals(Insets.of(1, 2, 3, 4), drawable.getOpticalInsets());
+ }
+
+ @Test
+ public void legacyShadowDrawableAPI() {
+ Drawable drawable = resources.getDrawable(R.drawable.vector_icon_create);
+ ShadowDrawable shadowDrawable = Shadow.extract(drawable);
+ assertEquals(R.drawable.vector_icon_create, shadowDrawable.getCreatedFromResId());
+ }
+
+ @Test
+ public void testTint() throws IOException {
+ Drawable drawable = resources.getDrawable(R.drawable.vector_icon_delete);
+ drawable = drawable.mutate();
+ drawable.setTint(Color.BLUE);
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ Bitmap output =
+ Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+ drawable.draw(canvas);
+ // Midpoint should be blue.
+ assertThat(output.getPixel(drawable.getIntrinsicWidth() / 2, drawable.getIntrinsicHeight() / 2))
+ .isEqualTo(Color.BLUE);
+ }
+
+ private void verifyPreloadDensityInner(Resources res, int densityDpi)
+ throws XmlPullParserException, IOException {
+ // Capture initial state at default density.
+ final XmlResourceParser parser =
+ DrawableTestUtils.getResourceParser(res, R.drawable.vector_density);
+ final VectorDrawable preloadedDrawable = new VectorDrawable();
+ preloadedDrawable.inflate(resources, parser, Xml.asAttributeSet(parser));
+ final ConstantState preloadedConstantState = preloadedDrawable.getConstantState();
+ final int origWidth = preloadedDrawable.getIntrinsicWidth();
+
+ // Set density to half of original. Unlike offsets, which are
+ // truncated, dimensions are rounded to the nearest pixel.
+ DrawableTestUtils.setResourcesDensity(res, densityDpi / 2);
+ final VectorDrawable halfDrawable = (VectorDrawable) preloadedConstantState.newDrawable(res);
+ // NOTE: densityDpi may not be an even number, so account for *actual* scaling in asserts
+ final float approxHalf = (float) (densityDpi / 2) / densityDpi;
+ assertEquals(Math.round(origWidth * approxHalf), halfDrawable.getIntrinsicWidth());
+
+ // Set density to double original.
+ DrawableTestUtils.setResourcesDensity(res, densityDpi * 2);
+ final VectorDrawable doubleDrawable = (VectorDrawable) preloadedConstantState.newDrawable(res);
+ assertEquals(origWidth * 2, doubleDrawable.getIntrinsicWidth());
+
+ // Restore original density.
+ DrawableTestUtils.setResourcesDensity(res, densityDpi);
+ final VectorDrawable origDrawable = (VectorDrawable) preloadedConstantState.newDrawable();
+ assertEquals(origWidth, origDrawable.getIntrinsicWidth());
+
+ // Ensure theme density is applied correctly.
+ final Theme t = res.newTheme();
+ halfDrawable.applyTheme(t);
+ assertEquals(origWidth, halfDrawable.getIntrinsicWidth());
+ doubleDrawable.applyTheme(t);
+ assertEquals(origWidth, doubleDrawable.getIntrinsicWidth());
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/BitmapComparer.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/BitmapComparer.java
new file mode 100644
index 000000000..ed7fac31f
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/BitmapComparer.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapcomparers;
+
+/** This abstract class can be used by the tester to implement their own comparison methods */
+public abstract class BitmapComparer {
+ /**
+ * Compares the two bitmaps given using Java.
+ *
+ * @param offset where in the bitmaps to start
+ * @param stride how much to skip between two different rows
+ * @param width the width of the subsection being tested
+ * @param height the height of the subsection being tested
+ */
+ public abstract boolean verifySame(
+ int[] ideal, int[] given, int offset, int stride, int width, int height);
+
+ /**
+ * This calculates the position in an array that would represent a bitmap given the parameters.
+ */
+ protected static int indexFromXAndY(int x, int y, int stride, int offset) {
+ return x + (y * stride) + offset;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/ExactComparer.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/ExactComparer.java
new file mode 100644
index 000000000..323847727
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/ExactComparer.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapcomparers;
+
+import android.util.Log;
+
+/** This class does an exact comparison of the pixels in a bitmap. */
+public class ExactComparer extends BitmapComparer {
+ private static final String TAG = "ExactComparer";
+
+ /** This method does an exact 1 to 1 comparison of the two bitmaps */
+ @Override
+ public boolean verifySame(
+ int[] ideal, int[] given, int offset, int stride, int width, int height) {
+ int count = 0;
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int index = indexFromXAndY(x, y, stride, offset);
+ if (ideal[index] != given[index]) {
+ if (count < 50) {
+ Log.d(TAG, "Failure on position x = " + x + " y = " + y);
+ Log.d(
+ TAG,
+ "Expected color : "
+ + Integer.toHexString(ideal[index])
+ + " given color : "
+ + Integer.toHexString(given[index]));
+ }
+ count++;
+ }
+ }
+ }
+ Log.d(TAG, "Number of different pixels : " + count);
+
+ return (count == 0);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/MSSIMComparer.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/MSSIMComparer.java
new file mode 100644
index 000000000..f5d0d003c
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapcomparers/MSSIMComparer.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapcomparers;
+
+import android.graphics.Color;
+import android.util.Log;
+
+/**
+ * Image comparison using Structural Similarity Index, developed by Wang, Bovik, Sheikh, and
+ * Simoncelli. Details can be read in their paper :
+ *
+ * <p>https://ece.uwaterloo.ca/~z70wang/publications/ssim.pdf
+ */
+public class MSSIMComparer extends BitmapComparer {
+ // These values were taken from the publication
+ public static final String TAG_NAME = "MSSIM";
+ public static final double CONSTANT_L = 254;
+ public static final double CONSTANT_K1 = 0.00001;
+ public static final double CONSTANT_K2 = 0.00003;
+ public static final double CONSTANT_C1 = Math.pow(CONSTANT_L * CONSTANT_K1, 2);
+ public static final double CONSTANT_C2 = Math.pow(CONSTANT_L * CONSTANT_K2, 2);
+ public static final int WINDOW_SIZE = 10;
+
+ private double threshold;
+
+ public MSSIMComparer(double threshold) {
+ this.threshold = threshold;
+ }
+
+ /**
+ * Compute the size of the window. The window defaults to WINDOW_SIZE, but must be contained
+ * within dimension.
+ */
+ private int computeWindowSize(int coordinateStart, int dimension) {
+ if (coordinateStart + WINDOW_SIZE <= dimension) {
+ return WINDOW_SIZE;
+ }
+ return dimension - coordinateStart;
+ }
+
+ @Override
+ public boolean verifySame(
+ int[] ideal, int[] given, int offset, int stride, int width, int height) {
+ double ssimTotal = 0;
+ int windows = 0;
+
+ for (int currentWindowY = 0; currentWindowY < height; currentWindowY += WINDOW_SIZE) {
+ int windowHeight = computeWindowSize(currentWindowY, height);
+ for (int currentWindowX = 0; currentWindowX < width; currentWindowX += WINDOW_SIZE) {
+ int windowWidth = computeWindowSize(currentWindowX, width);
+ int start = indexFromXAndY(currentWindowX, currentWindowY, stride, offset);
+ if (isWindowWhite(ideal, start, stride, windowWidth, windowHeight)
+ && isWindowWhite(given, start, stride, windowWidth, windowHeight)) {
+ continue;
+ }
+ windows++;
+ double[] means = getMeans(ideal, given, start, stride, windowWidth, windowHeight);
+ double meanX = means[0];
+ double meanY = means[1];
+ double[] variances =
+ getVariances(ideal, given, meanX, meanY, start, stride, windowWidth, windowHeight);
+ double varX = variances[0];
+ double varY = variances[1];
+ double stdBoth = variances[2];
+ double ssim = ssim(meanX, meanY, varX, varY, stdBoth);
+ ssimTotal += ssim;
+ }
+ }
+
+ if (windows == 0) {
+ return true;
+ }
+
+ ssimTotal /= windows;
+
+ Log.d(TAG_NAME, "MSSIM = " + ssimTotal);
+
+ return (ssimTotal >= threshold);
+ }
+
+ private boolean isWindowWhite(
+ int[] colors, int start, int stride, int windowWidth, int windowHeight) {
+ for (int y = 0; y < windowHeight; y++) {
+ for (int x = 0; x < windowWidth; x++) {
+ if (colors[indexFromXAndY(x, y, stride, start)] != Color.WHITE) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private double ssim(double muX, double muY, double sigX, double sigY, double sigXY) {
+ double ssimDouble = (((2 * muX * muY) + CONSTANT_C1) * ((2 * sigXY) + CONSTANT_C2));
+ double denom = ((muX * muX) + (muY * muY) + CONSTANT_C1) * (sigX + sigY + CONSTANT_C2);
+ ssimDouble /= denom;
+ return ssimDouble;
+ }
+
+ /**
+ * This method will find the mean of a window in both sets of pixels. The return is an array where
+ * the first double is the mean of the first set and the second double is the mean of the second
+ * set.
+ */
+ private double[] getMeans(
+ int[] pixels0, int[] pixels1, int start, int stride, int windowWidth, int windowHeight) {
+ double avg0 = 0;
+ double avg1 = 0;
+ for (int y = 0; y < windowHeight; y++) {
+ for (int x = 0; x < windowWidth; x++) {
+ int index = indexFromXAndY(x, y, stride, start);
+ avg0 += getIntensity(pixels0[index]);
+ avg1 += getIntensity(pixels1[index]);
+ }
+ }
+ avg0 /= windowWidth * windowHeight;
+ avg1 /= windowWidth * windowHeight;
+ return new double[] {avg0, avg1};
+ }
+
+ /**
+ * Finds the variance of the two sets of pixels, as well as the covariance of the windows. The
+ * return value is an array of doubles, the first is the variance of the first set of pixels, the
+ * second is the variance of the second set of pixels, and the third is the covariance.
+ */
+ private double[] getVariances(
+ int[] pixels0,
+ int[] pixels1,
+ double mean0,
+ double mean1,
+ int start,
+ int stride,
+ int windowWidth,
+ int windowHeight) {
+ double var0 = 0;
+ double var1 = 0;
+ double varBoth = 0;
+ for (int y = 0; y < windowHeight; y++) {
+ for (int x = 0; x < windowWidth; x++) {
+ int index = indexFromXAndY(x, y, stride, start);
+ double v0 = getIntensity(pixels0[index]) - mean0;
+ double v1 = getIntensity(pixels1[index]) - mean1;
+ var0 += v0 * v0;
+ var1 += v1 * v1;
+ varBoth += v0 * v1;
+ }
+ }
+ var0 /= (windowWidth * windowHeight) - 1;
+ var1 /= (windowWidth * windowHeight) - 1;
+ varBoth /= (windowWidth * windowHeight) - 1;
+ return new double[] {var0, var1, varBoth};
+ }
+
+ /**
+ * Gets the intensity of a given pixel in RGB using luminosity formula
+ *
+ * <p>l = 0.21R' + 0.72G' + 0.07B'
+ *
+ * <p>The prime symbols dictate a gamma correction of 1.
+ */
+ private double getIntensity(int pixel) {
+ final double gamma = 1;
+ double l = 0;
+ l += (0.21f * Math.pow(Color.red(pixel) / 255f, gamma));
+ l += (0.72f * Math.pow(Color.green(pixel) / 255f, gamma));
+ l += (0.07f * Math.pow(Color.blue(pixel) / 255f, gamma));
+ return l;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/BitmapVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/BitmapVerifier.java
new file mode 100644
index 000000000..32e4057f8
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/BitmapVerifier.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+/** Checks to see if a Bitmap follows the algorithm provided by the verifier */
+public abstract class BitmapVerifier {
+ protected static final int PASS_COLOR = Color.WHITE;
+ protected static final int FAIL_COLOR = Color.RED;
+
+ protected Bitmap differenceBitmapBase;
+
+ public boolean verify(Bitmap bitmap) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int[] pixels = new int[width * height];
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
+ return verify(pixels, 0, width, width, height);
+ }
+
+ /** This will test if the bitmap is good or not. */
+ public abstract boolean verify(int[] bitmap, int offset, int stride, int width, int height);
+
+ /**
+ * This calculates the position in an array that would represent a bitmap given the parameters.
+ */
+ protected static int indexFromXAndY(int x, int y, int stride, int offset) {
+ return x + (y * stride) + offset;
+ }
+
+ public Bitmap getDifferenceBitmap() {
+ return differenceBitmapBase;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/BlurPixelVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/BlurPixelVerifier.java
new file mode 100644
index 000000000..7a210e59a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/BlurPixelVerifier.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import android.graphics.Color;
+
+public class BlurPixelVerifier extends BitmapVerifier {
+
+ private final int dstColor;
+ private final int srcColor;
+
+ /**
+ * Create a BitmapVerifier that compares pixel values relative to the provided source and
+ * destination colors. Pixels closer to the center of the test bitmap are expected to match closer
+ * to the source color, while pixels on the exterior of the test bitmap are expected to match the
+ * destination color more closely
+ */
+ public BlurPixelVerifier(int srcColor, int dstColor) {
+ this.srcColor = srcColor;
+ this.dstColor = dstColor;
+ }
+
+ @Override
+ public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
+
+ float dstRedChannel = Color.red(dstColor);
+ float dstGreenChannel = Color.green(dstColor);
+ float dstBlueChannel = Color.blue(dstColor);
+
+ float srcRedChannel = Color.red(srcColor);
+ float srcGreenChannel = Color.green(srcColor);
+ float srcBlueChannel = Color.blue(srcColor);
+
+ // Calculate the largest rgb color difference between the source and destination
+ // colors
+ double maxDifference =
+ Math.pow(srcRedChannel - dstRedChannel, 2.0f)
+ + Math.pow(srcGreenChannel - dstGreenChannel, 2.0f)
+ + Math.pow(srcBlueChannel - dstBlueChannel, 2.0f);
+
+ // Calculate the maximum distance between pixels to the center of the test image
+ double maxPixelDistance = Math.sqrt(Math.pow(width / 2.0, 2.0) + Math.pow(height / 2.0, 2.0));
+
+ // Additional tolerance applied to comparisons
+ float threshold = .05f;
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ double pixelDistance =
+ Math.sqrt(Math.pow(x - width / 2.0, 2.0) + Math.pow(y - height / 2.0, 2.0));
+ // Calculate the threshold of the destination color expected based on the
+ // pixels position relative to the center
+ double dstPercentage = pixelDistance / maxPixelDistance + threshold;
+
+ int pixelColor = bitmap[indexFromXAndY(x, y, stride, offset)];
+ double pixelRedChannel = Color.red(pixelColor);
+ double pixelGreenChannel = Color.green(pixelColor);
+ double pixelBlueChannel = Color.blue(pixelColor);
+ // Compare the RGB color distance between the current pixel and the destination
+ // color
+ double dstDistance =
+ Math.sqrt(
+ Math.pow(pixelRedChannel - dstRedChannel, 2.0)
+ + Math.pow(pixelGreenChannel - dstGreenChannel, 2.0)
+ + Math.pow(pixelBlueChannel - dstBlueChannel, 2.0));
+
+ // Compare the RGB color distance between the current pixel and the source
+ // color
+ double srcDistance =
+ Math.sqrt(
+ Math.pow(pixelRedChannel - srcRedChannel, 2.0)
+ + Math.pow(pixelGreenChannel - srcGreenChannel, 2.0)
+ + Math.pow(pixelBlueChannel - srcBlueChannel, 2.0));
+
+ // calculate the ratio between the destination color to the current pixel
+ // color relative to the maximum distance between source and destination colors
+ // If this value exceeds the threshold expected for the pixel distance from
+ // center then we are rendering an unexpected color
+ double dstFraction = dstDistance / maxDifference;
+ if (dstFraction > dstPercentage) {
+ return false;
+ }
+
+ // similarly compute the ratio between the source color to the current pixel
+ // color relative to the maximum distance between source and destination colors
+ // If this value exceeds the threshold expected for the pixel distance from
+ // center then we are rendering an unexpected source color
+ double srcFraction = srcDistance / maxDifference;
+ if (srcFraction > dstPercentage) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/ColorCountVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/ColorCountVerifier.java
new file mode 100644
index 000000000..c37b77318
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/ColorCountVerifier.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import android.util.Log;
+import org.robolectric.integrationtests.nativegraphics.testing.util.CompareUtils;
+
+public class ColorCountVerifier extends BitmapVerifier {
+ private int color;
+ private int count;
+ private int threshold;
+
+ public ColorCountVerifier(int color, int count, int threshold) {
+ this.color = color;
+ this.count = count;
+ this.threshold = threshold;
+ }
+
+ public ColorCountVerifier(int color, int count) {
+ this(color, count, 0);
+ }
+
+ @Override
+ public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
+ int count = 0;
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ if (CompareUtils.verifyPixelWithThreshold(
+ bitmap[indexFromXAndY(x, y, stride, offset)], color, threshold)) {
+ count++;
+ }
+ }
+ }
+ if (count != this.count) {
+ Log.d("ColorCountVerifier", ("Color count mismatch " + count) + " != " + this.count);
+ }
+ return count == this.count;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/ColorVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/ColorVerifier.java
new file mode 100644
index 000000000..27787326a
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/ColorVerifier.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import androidx.annotation.ColorInt;
+
+/** Checks to see if a bitmap is entirely a single color */
+public class ColorVerifier extends PerPixelBitmapVerifier {
+ @ColorInt private int color;
+
+ public ColorVerifier(@ColorInt int color) {
+ this(color, DEFAULT_THRESHOLD);
+ }
+
+ public ColorVerifier(@ColorInt int color, int colorTolerance) {
+ super(colorTolerance);
+ this.color = color;
+ }
+
+ public ColorVerifier(@ColorInt int color, int colorThreshold, float spatialTolerance) {
+ super(colorThreshold, spatialTolerance);
+ this.color = color;
+ }
+
+ @Override
+ @ColorInt
+ protected int getExpectedColor(int x, int y) {
+ return color;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/GoldenImageVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/GoldenImageVerifier.java
new file mode 100644
index 000000000..ba7f9bdf6
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/GoldenImageVerifier.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import org.robolectric.integrationtests.nativegraphics.testing.bitmapcomparers.BitmapComparer;
+import org.robolectric.integrationtests.nativegraphics.testing.differencevisualizers.PassFailVisualizer;
+
+public class GoldenImageVerifier extends BitmapVerifier {
+ private final BitmapComparer bitmapComparer;
+ private final int[] goldenBitmapArray;
+ private final int width;
+ private final int height;
+
+ public GoldenImageVerifier(Bitmap goldenBitmap, BitmapComparer bitmapComparer) {
+ width = goldenBitmap.getWidth();
+ height = goldenBitmap.getHeight();
+ goldenBitmapArray = new int[width * height];
+ goldenBitmap.getPixels(goldenBitmapArray, 0, width, 0, 0, width, height);
+ this.bitmapComparer = bitmapComparer;
+ }
+
+ public GoldenImageVerifier(Context context, int goldenResId, BitmapComparer bitmapComparer) {
+ this(BitmapFactory.decodeResource(context.getResources(), goldenResId), bitmapComparer);
+ }
+
+ @Override
+ public boolean verify(Bitmap bitmap) {
+ // Clip to the size of the golden image.
+ if (bitmap.getWidth() > width || bitmap.getHeight() > height) {
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
+ }
+ return super.verify(bitmap);
+ }
+
+ @Override
+ public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
+ boolean success =
+ bitmapComparer.verifySame(goldenBitmapArray, bitmap, offset, stride, width, height);
+ if (!success) {
+ differenceBitmapBase = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ int[] differences = new PassFailVisualizer().getDifferences(goldenBitmapArray, bitmap);
+ differenceBitmapBase.setPixels(differences, 0, width, 0, 0, width, height);
+ }
+ return success;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/PerPixelBitmapVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/PerPixelBitmapVerifier.java
new file mode 100644
index 000000000..b0d5f7688
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/PerPixelBitmapVerifier.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.util.Log;
+import androidx.annotation.ColorInt;
+import org.robolectric.integrationtests.nativegraphics.testing.util.CompareUtils;
+
+/** This class looks at every pixel in a given bitmap and verifies that it is correct. */
+public abstract class PerPixelBitmapVerifier extends BitmapVerifier {
+ private static final String TAG = "PerPixelBitmapVerifer";
+ public static final int DEFAULT_THRESHOLD = 48;
+
+ // total color difference tolerated without the pixel failing
+ private int colorTolerance;
+
+ // portion of bitmap allowed to fail pixel check
+ private float spatialTolerance;
+
+ public PerPixelBitmapVerifier() {
+ this(DEFAULT_THRESHOLD, 0);
+ }
+
+ public PerPixelBitmapVerifier(int colorTolerance) {
+ this(colorTolerance, 0);
+ }
+
+ public PerPixelBitmapVerifier(int colorTolerance, float spatialTolerance) {
+ this.colorTolerance = colorTolerance;
+ this.spatialTolerance = spatialTolerance;
+ }
+
+ @ColorInt
+ protected int getExpectedColor(int x, int y) {
+ return Color.WHITE;
+ }
+
+ @Override
+ public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
+ int failures = 0;
+ int[] differenceMap = new int[bitmap.length];
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int index = indexFromXAndY(x, y, stride, offset);
+ if (!verifyPixel(x, y, bitmap[index])) {
+ if (failures < 50) {
+ Log.d(
+ TAG,
+ "Expected : "
+ + Integer.toHexString(getExpectedColor(x, y))
+ + " received : "
+ + Integer.toHexString(bitmap[index])
+ + " at position ("
+ + x
+ + ","
+ + y
+ + ")");
+ }
+ failures++;
+ differenceMap[index] = FAIL_COLOR;
+ } else {
+ differenceMap[index] = PASS_COLOR;
+ }
+ }
+ }
+ int toleratedFailures = (int) (spatialTolerance * width * height);
+ boolean success = failures <= toleratedFailures;
+ Log.d(TAG, failures + " failures observed out of " + toleratedFailures + " tolerated failures");
+ if (!success) {
+ differenceBitmapBase = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ differenceBitmapBase.setPixels(differenceMap, offset, stride, 0, 0, width, height);
+ }
+ return success;
+ }
+
+ protected boolean verifyPixel(int x, int y, int observedColor) {
+ int expectedColor = getExpectedColor(x, y);
+ return CompareUtils.verifyPixelWithThreshold(observedColor, expectedColor, colorTolerance);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/RectVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/RectVerifier.java
new file mode 100644
index 000000000..f2c1de2d4
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/RectVerifier.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import android.graphics.Rect;
+
+/** Tests to see if there is rectangle of a certain color, with a background given */
+public class RectVerifier extends PerPixelBitmapVerifier {
+ private int outerColor;
+ private int innerColor;
+ private Rect innerRect;
+
+ public RectVerifier(int outerColor, int innerColor, Rect innerRect) {
+ this(outerColor, innerColor, innerRect, DEFAULT_THRESHOLD);
+ }
+
+ public RectVerifier(int outerColor, int innerColor, Rect innerRect, int tolerance) {
+ super(tolerance);
+ this.outerColor = outerColor;
+ this.innerColor = innerColor;
+ this.innerRect = innerRect;
+ }
+
+ @Override
+ protected int getExpectedColor(int x, int y) {
+ return innerRect.contains(x, y) ? innerColor : outerColor;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/RegionVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/RegionVerifier.java
new file mode 100644
index 000000000..352ef7a3b
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/RegionVerifier.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import static org.junit.Assert.assertFalse;
+
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RegionVerifier extends BitmapVerifier {
+ private static class SubRegionVerifiers {
+ public Region region;
+ public BitmapVerifier verifier;
+
+ SubRegionVerifiers(Region region, BitmapVerifier verifier) {
+ this.region = region;
+ this.verifier = verifier;
+ }
+ }
+
+ private List<SubRegionVerifiers> regionVerifiers = new ArrayList<>();
+
+ @Override
+ public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
+ assertFalse(regionVerifiers.isEmpty());
+ boolean isVerified = true;
+ for (SubRegionVerifiers subRegionVerifier : regionVerifiers) {
+ if (subRegionVerifier.region.isRect()) {
+ Rect area = subRegionVerifier.region.getBounds();
+ isVerified &= verifySubRect(bitmap, offset, stride, subRegionVerifier.verifier, area);
+ } else {
+ RegionIterator iter = new RegionIterator(subRegionVerifier.region);
+ Rect area = new Rect();
+ while (iter.next(area)) {
+ isVerified &= verifySubRect(bitmap, offset, stride, subRegionVerifier.verifier, area);
+ }
+ }
+ }
+ return isVerified;
+ }
+
+ private boolean verifySubRect(
+ int[] bitmap, int offset, int stride, BitmapVerifier subVerifier, Rect rect) {
+ final int newOffset = rect.top * stride + rect.left + offset;
+ return subVerifier.verify(bitmap, newOffset, stride, rect.width(), rect.height());
+ }
+
+ @CanIgnoreReturnValue
+ public RegionVerifier addVerifier(Rect area, BitmapVerifier verifier) {
+ return addVerifier(new Region(area), verifier);
+ }
+
+ @CanIgnoreReturnValue
+ public RegionVerifier addVerifier(Region area, BitmapVerifier verifier) {
+ regionVerifiers.add(new SubRegionVerifiers(area, verifier));
+ return this;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/SamplePointWideGamutVerifier.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/SamplePointWideGamutVerifier.java
new file mode 100644
index 000000000..44cd05af7
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/bitmapverifiers/SamplePointWideGamutVerifier.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics.testing.bitmapverifiers;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.util.Log;
+import org.junit.Assert;
+
+public class SamplePointWideGamutVerifier extends BitmapVerifier {
+ private static final String TAG = "SamplePointWideGamut";
+
+ private final Point[] points;
+ private final Color[] colors;
+ private final float eps;
+
+ public SamplePointWideGamutVerifier(Point[] points, Color[] colors, float eps) {
+ this.points = points;
+ this.colors = colors;
+ this.eps = eps;
+ }
+
+ @Override
+ public boolean verify(Bitmap bitmap) {
+ Assert.assertTrue(
+ "You cannot use this verifier with a bitmap whose ColorSpace is not "
+ + "wide gamut: "
+ + bitmap.getColorSpace(),
+ bitmap.getColorSpace().isWideGamut());
+
+ boolean success = true;
+ for (int i = 0; i < points.length; i++) {
+ Point p = points[i];
+ Color expected = colors[i];
+
+ Color actual = bitmap.getColor(p.x, p.y).convert(expected.getColorSpace());
+
+ boolean localSuccess = true;
+ if (!floatCompare(expected.red(), actual.red(), eps)) {
+ localSuccess = false;
+ }
+ if (!floatCompare(expected.green(), actual.green(), eps)) {
+ localSuccess = false;
+ }
+ if (!floatCompare(expected.blue(), actual.blue(), eps)) {
+ localSuccess = false;
+ }
+ if (!floatCompare(expected.alpha(), actual.alpha(), eps)) {
+ localSuccess = false;
+ }
+
+ if (!localSuccess) {
+ success = false;
+ Log.w(TAG, "Expected " + expected + " at " + p + ", got " + actual);
+ }
+ }
+ return success;
+ }
+
+ @Override
+ public boolean verify(int[] bitmap, int offset, int stride, int width, int height) {
+ Assert.fail("This verifier requires more info than can be encoded in sRGB (int) values");
+ return false;
+ }
+
+ private static boolean floatCompare(float a, float b, float eps) {
+ return Float.compare(a, b) == 0 || Math.abs(a - b) <= eps;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/differencevisualizers/DifferenceVisualizer.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/differencevisualizers/DifferenceVisualizer.java
new file mode 100644
index 000000000..85d31ba16
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/differencevisualizers/DifferenceVisualizer.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.differencevisualizers;
+
+/** This class can be extended by the tester, to allow for various ways to debug. */
+public abstract class DifferenceVisualizer {
+ public abstract int[] getDifferences(int[] ideal, int[] given);
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/differencevisualizers/PassFailVisualizer.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/differencevisualizers/PassFailVisualizer.java
new file mode 100644
index 000000000..c96c6fd07
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/differencevisualizers/PassFailVisualizer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.robolectric.integrationtests.nativegraphics.testing.differencevisualizers;
+
+import android.graphics.Color;
+
+/** This class creates difference maps that show which pixels were correct, and which weren't */
+public class PassFailVisualizer extends DifferenceVisualizer {
+ /**
+ * This method will return a bitmap where white is same red is different
+ *
+ * @param ideal the desired result
+ * @param given the produced result
+ */
+ @Override
+ public int[] getDifferences(int[] ideal, int[] given) {
+ int[] output = new int[ideal.length];
+ for (int y = 0; y < output.length; y++) {
+ if (ideal[y] == given[y]) {
+ output[y] = Color.WHITE;
+ } else {
+ output[y] = Color.RED;
+ }
+ }
+ return output;
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/text/EditorState.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/text/EditorState.java
new file mode 100644
index 000000000..e67471e41
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/text/EditorState.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics.testing.text;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ReplacementSpan;
+import com.google.common.base.Splitter;
+import junit.framework.Assert;
+
+/**
+ * Represents an editor state.
+ *
+ * <p>The editor state can be specified by following string format. - Components are separated by
+ * space(U+0020). - Single-quoted string for printable ASCII characters, e.g. 'a', '123'. - U+XXXX
+ * form can be used for a Unicode code point. - Components inside '[' and ']' are in selection. -
+ * Components inside '(' and ')' are in ReplacementSpan. - '|' is for specifying cursor position.
+ *
+ * <p>Selection and cursor can not be specified at the same time.
+ *
+ * <p>Example: - "'Hello,' | U+0020 'world!'" means "Hello, world!" is displayed and the cursor
+ * position is 6. - "'abc' [ 'def' ] 'ghi'" means "abcdefghi" is displayed and "def" is selected. -
+ * "U+1F441 | ( U+1F441 U+1F441 )" means three U+1F441 characters are displayed and ReplacementSpan
+ * is set from offset 2 to 6.
+ */
+public class EditorState {
+ private static final String REPLACEMENT_SPAN_START = "(";
+ private static final String REPLACEMENT_SPAN_END = ")";
+ private static final String SELECTION_START = "[";
+ private static final String SELECTION_END = "]";
+ private static final String CURSOR = "|";
+
+ public Editable text;
+ public int selectionStart = -1;
+ public int selectionEnd = -1;
+
+ public EditorState() {}
+
+ /** A mocked {@link android.text.style.ReplacementSpan} for testing purpose. */
+ private static class MockReplacementSpan extends ReplacementSpan {
+ @Override
+ public int getSize(
+ Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
+ return 0;
+ }
+
+ @Override
+ public void draw(
+ Canvas canvas,
+ CharSequence text,
+ int start,
+ int end,
+ float x,
+ int top,
+ int y,
+ int bottom,
+ Paint paint) {}
+ }
+
+ // Returns true if the code point is ASCII and graph.
+ private boolean isGraphicAscii(int codePoint) {
+ return 0x20 < codePoint && codePoint < 0x7F;
+ }
+
+ // Setup editor state with string. Please see class description for string format.
+ public void setByString(String string) {
+ final StringBuilder sb = new StringBuilder();
+ int replacementSpanStart = -1;
+ int replacementSpanEnd = -1;
+ selectionStart = -1;
+ selectionEnd = -1;
+
+ final Iterable<String> tokens = Splitter.onPattern(" +").split(string);
+ for (String token : tokens) {
+ if (token.startsWith("'") && token.endsWith("'")) {
+ for (int i = 1; i < token.length() - 1; ++i) {
+ final char ch = token.charAt(1);
+ if (!isGraphicAscii(ch)) {
+ throw new IllegalArgumentException(
+ "Only printable characters can be in single quote. "
+ + "Use U+"
+ + Integer.toHexString(ch).toUpperCase()
+ + " instead");
+ }
+ }
+ sb.append(token.substring(1, token.length() - 1));
+ } else if (token.startsWith("U+")) {
+ final int codePoint = Integer.parseInt(token.substring(2), 16);
+ if (codePoint < 0 || 0x10FFFF < codePoint) {
+ throw new IllegalArgumentException("Invalid code point is specified:" + token);
+ }
+ sb.append(Character.toChars(codePoint));
+ } else if (token.equals(CURSOR)) {
+ if (selectionStart != -1 || selectionEnd != -1) {
+ throw new IllegalArgumentException(
+ "Two or more cursor/selection positions are specified.");
+ }
+ selectionStart = selectionEnd = sb.length();
+ } else if (token.equals(SELECTION_START)) {
+ if (selectionStart != -1) {
+ throw new IllegalArgumentException(
+ "Two or more cursor/selection positions are specified.");
+ }
+ selectionStart = sb.length();
+ } else if (token.equals(SELECTION_END)) {
+ if (selectionEnd != -1) {
+ throw new IllegalArgumentException(
+ "Two or more cursor/selection positions are specified.");
+ }
+ selectionEnd = sb.length();
+ } else if (token.equals(REPLACEMENT_SPAN_START)) {
+ if (replacementSpanStart != -1) {
+ throw new IllegalArgumentException("Only one replacement span is supported");
+ }
+ replacementSpanStart = sb.length();
+ } else if (token.equals(REPLACEMENT_SPAN_END)) {
+ if (replacementSpanEnd != -1) {
+ throw new IllegalArgumentException("Only one replacement span is supported");
+ }
+ replacementSpanEnd = sb.length();
+ } else {
+ throw new IllegalArgumentException("Unknown or invalid token: " + token);
+ }
+ }
+
+ if (selectionStart == -1 || selectionEnd == -1) {
+ if (selectionEnd != -1) {
+ throw new IllegalArgumentException("Selection start position doesn't exist.");
+ } else if (selectionStart != -1) {
+ throw new IllegalArgumentException("Selection end position doesn't exist.");
+ } else {
+ throw new IllegalArgumentException(
+ "At least cursor position or selection range must be specified.");
+ }
+ } else if (selectionStart > selectionEnd) {
+ throw new IllegalArgumentException("Selection start position appears after end position.");
+ }
+
+ final Spannable spannable = new SpannableString(sb.toString());
+
+ if (replacementSpanStart != -1 || replacementSpanEnd != -1) {
+ if (replacementSpanStart == -1) {
+ throw new IllegalArgumentException("ReplacementSpan start position doesn't exist.");
+ }
+ if (replacementSpanEnd == -1) {
+ throw new IllegalArgumentException("ReplacementSpan end position doesn't exist.");
+ }
+ if (replacementSpanStart > replacementSpanEnd) {
+ throw new IllegalArgumentException(
+ "ReplacementSpan start position appears after end position.");
+ }
+ spannable.setSpan(
+ new MockReplacementSpan(),
+ replacementSpanStart,
+ replacementSpanEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ text = Editable.Factory.getInstance().newEditable(spannable);
+ }
+
+ public void assertEquals(String string) {
+ EditorState expected = new EditorState();
+ expected.setByString(string);
+
+ Assert.assertEquals(expected.text.toString(), text.toString());
+ Assert.assertEquals(expected.selectionStart, selectionStart);
+ Assert.assertEquals(expected.selectionEnd, selectionEnd);
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/AssertionError.kt b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/AssertionError.kt
new file mode 100644
index 000000000..277bf7644
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/AssertionError.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics.testing.util
+
+/**
+ * Helper to use ThrowNew from JNI to throw an AssertionError.
+ *
+ * ThrowNew calls <init>(String), but that constructor for AssertionError is private. In this case,
+ * there is no Throwable cause, so simplify the native code.
+ */
+class AssertionError(msg: String) : java.lang.AssertionError(msg, null)
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/CompareUtils.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/CompareUtils.java
new file mode 100644
index 000000000..a3dd42f1d
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/CompareUtils.java
@@ -0,0 +1,31 @@
+package org.robolectric.integrationtests.nativegraphics.testing.util;
+
+import android.graphics.Color;
+
+public final class CompareUtils {
+ /**
+ * @return True if close enough
+ */
+ public static boolean verifyPixelWithThreshold(int color, int expectedColor, int threshold) {
+ int diff =
+ Math.abs(Color.red(color) - Color.red(expectedColor))
+ + Math.abs(Color.green(color) - Color.green(expectedColor))
+ + Math.abs(Color.blue(color) - Color.blue(expectedColor));
+ return diff <= threshold;
+ }
+
+ /**
+ * @param threshold Per channel differences for R / G / B channel against the average of these 3
+ * channels. Should be less than 2 normally.
+ * @return True if the color is close enough to be a gray scale color.
+ */
+ public static boolean verifyPixelGrayScale(int color, int threshold) {
+ int average = Color.red(color) + Color.green(color) + Color.blue(color);
+ average /= 3;
+ return Math.abs(Color.red(color) - average) <= threshold
+ && Math.abs(Color.green(color) - average) <= threshold
+ && Math.abs(Color.blue(color) - average) <= threshold;
+ }
+
+ private CompareUtils() {}
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/DrawCountDown.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/DrawCountDown.java
new file mode 100644
index 000000000..e1ad49593
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/DrawCountDown.java
@@ -0,0 +1,53 @@
+package org.robolectric.integrationtests.nativegraphics.testing.util;
+
+import android.view.View;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import java.util.HashSet;
+import java.util.Set;
+
+public class DrawCountDown implements OnPreDrawListener {
+ private static Set<DrawCountDown> pendingCallbacks = new HashSet<>();
+
+ private int drawCount;
+ private View targetView;
+ private Runnable runnable;
+
+ private DrawCountDown(View targetView, int countFrames, Runnable countReachedListener) {
+ this.targetView = targetView;
+ drawCount = countFrames;
+ runnable = countReachedListener;
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ if (drawCount <= 0) {
+ synchronized (pendingCallbacks) {
+ pendingCallbacks.remove(this);
+ }
+ targetView.getViewTreeObserver().removeOnPreDrawListener(this);
+ runnable.run();
+ } else {
+ drawCount--;
+ targetView.postInvalidate();
+ }
+ return true;
+ }
+
+ public static void countDownDraws(
+ View targetView, int countFrames, Runnable onDrawCountReachedListener) {
+ DrawCountDown counter = new DrawCountDown(targetView, countFrames, onDrawCountReachedListener);
+ synchronized (pendingCallbacks) {
+ pendingCallbacks.add(counter);
+ }
+ targetView.getViewTreeObserver().addOnPreDrawListener(counter);
+ }
+
+ public static void cancelPending() {
+ synchronized (pendingCallbacks) {
+ for (DrawCountDown counter : pendingCallbacks) {
+ counter.targetView.getViewTreeObserver().removeOnPreDrawListener(counter);
+ }
+ pendingCallbacks.clear();
+ }
+ }
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/SneakyThrow.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/SneakyThrow.java
new file mode 100644
index 000000000..6b4367a88
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/SneakyThrow.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.robolectric.integrationtests.nativegraphics.testing.util;
+
+/**
+ * Provides a hacky method that always throws {@code t} even if {@code t} is a checked exception.
+ * and is not declared to be thrown.
+ *
+ * <p>See http://www.mail-archive.com/javaposse@googlegroups.com/msg05984.html
+ */
+public final class SneakyThrow {
+ /**
+ * A hacky method that always throws {@code t} even if {@code t} is a checked exception, and is
+ * not declared to be thrown.
+ */
+ public static void sneakyThrow(Throwable t) {
+ SneakyThrow.<RuntimeException>sneakyThrowInternal(t);
+ }
+
+ @SuppressWarnings({"unchecked"})
+ private static <T extends Throwable> void sneakyThrowInternal(Throwable t) throws T {
+ throw (T) t;
+ }
+
+ private SneakyThrow() {}
+}
diff --git a/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/WebViewReadyHelper.java b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/WebViewReadyHelper.java
new file mode 100644
index 000000000..80e96c982
--- /dev/null
+++ b/integration_tests/nativegraphics/src/test/java/org/robolectric/integrationtests/nativegraphics/testing/util/WebViewReadyHelper.java
@@ -0,0 +1,61 @@
+package org.robolectric.integrationtests.nativegraphics.testing.util;
+
+import android.view.ViewTreeObserver.OnDrawListener;
+import android.webkit.WebView;
+import android.webkit.WebView.VisualStateCallback;
+import android.webkit.WebViewClient;
+import java.util.concurrent.CountDownLatch;
+
+public final class WebViewReadyHelper {
+ // Hacky quick-fix similar to DrawActivity's DrawCounterListener
+ // TODO: De-dupe this against DrawCounterListener and fix this cruft
+ private static final int DEBUG_REQUIRE_EXTRA_FRAMES = 1;
+ private int drawCount = 0;
+
+ private final CountDownLatch latch;
+ private final WebView webView;
+
+ public WebViewReadyHelper(WebView webView, CountDownLatch latch) {
+ this.webView = webView;
+ this.latch = latch;
+ this.webView.setWebViewClient(client);
+ }
+
+ public void loadData(String data) {
+ webView.loadData(data, null, null);
+ }
+
+ private WebViewClient client =
+ new WebViewClient() {
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ webView.postVisualStateCallback(0, visualStateCallback);
+ }
+ };
+
+ private VisualStateCallback visualStateCallback =
+ new VisualStateCallback() {
+ @Override
+ public void onComplete(long requestId) {
+ webView.getViewTreeObserver().addOnDrawListener(onDrawListener);
+ webView.invalidate();
+ }
+ };
+
+ private OnDrawListener onDrawListener =
+ new OnDrawListener() {
+ @Override
+ public void onDraw() {
+ if (++drawCount <= DEBUG_REQUIRE_EXTRA_FRAMES) {
+ webView.postInvalidate();
+ return;
+ }
+
+ webView.post(
+ () -> {
+ webView.getViewTreeObserver().removeOnDrawListener(onDrawListener);
+ latch.countDown();
+ });
+ }
+ };
+}
diff --git a/integration_tests/play_services/build.gradle b/integration_tests/play_services/build.gradle
index 409ee22af..f7499dd73 100644
--- a/integration_tests/play_services/build.gradle
+++ b/integration_tests/play_services/build.gradle
@@ -9,6 +9,7 @@ dependencies {
testCompileOnly AndroidSdk.MAX_SDK.coordinates
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
- testImplementation "junit:junit:${junitVersion}"
+ testImplementation "junit:junit:$junitVersion"
+ testImplementation "com.google.truth:truth:$truthVersion"
testImplementation "com.google.android.gms:play-services-basement:18.0.1"
} \ No newline at end of file
diff --git a/integration_tests/security-providers/build.gradle b/integration_tests/security-providers/build.gradle
index 69d605b3d..f96df56b9 100644
--- a/integration_tests/security-providers/build.gradle
+++ b/integration_tests/security-providers/build.gradle
@@ -11,5 +11,5 @@ dependencies {
testImplementation "com.google.truth:truth:${truthVersion}"
testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.4.0"
testImplementation "com.squareup.okhttp3:okhttp"
- testImplementation platform("com.squareup.okhttp3:okhttp-bom:4.8.0")
+ testImplementation platform("com.squareup.okhttp3:okhttp-bom:4.10.0")
}
diff --git a/integration_tests/sparsearray/build.gradle b/integration_tests/sparsearray/build.gradle
index 4aee7f6b4..762717794 100644
--- a/integration_tests/sparsearray/build.gradle
+++ b/integration_tests/sparsearray/build.gradle
@@ -8,7 +8,7 @@ apply plugin: "com.diffplug.spotless"
spotless {
kotlin {
target '**/*.kt'
- ktfmt('0.34').googleStyle()
+ ktfmt('0.42').googleStyle()
}
}
diff --git a/junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java b/junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java
index fb5a87fda..bfa00538b 100644
--- a/junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java
+++ b/junit/src/main/java/org/robolectric/internal/SandboxTestRunner.java
@@ -297,11 +297,10 @@ public class SandboxTestRunner extends BlockJUnit4ClassRunner {
} catch (Exception e) {
e.printStackTrace();
}
+ reportPerfStats(perfStatsCollector);
+ perfStatsCollector.reset();
}
});
-
- reportPerfStats(perfStatsCollector);
- perfStatsCollector.reset();
}
};
}
diff --git a/nativeruntime/build.gradle b/nativeruntime/build.gradle
index 495bf65f4..1ef93175a 100644
--- a/nativeruntime/build.gradle
+++ b/nativeruntime/build.gradle
@@ -6,178 +6,58 @@ import org.robolectric.gradle.RoboJavaModulePlugin
apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
-static def osName() {
- def osName = System.getProperty("os.name").toLowerCase(Locale.US);
- if (osName.contains("linux")) {
- return "linux"
- } else if (osName.contains("mac")) {
- return "mac"
- } else if (osName.contains("win")) {
- return "windows"
- }
- return "unknown"
-}
-
-static def arch() {
- def arch = System.getProperty("os.arch").toLowerCase(Locale.US);
- if (arch.equals("x86_64") || arch.equals("amd64")) {
- return "x86_64"
- }
- return arch
-}
+if (System.getenv('PUBLISH_NATIVERUNTIME_DIST_COMPAT') == "true") {
+ apply plugin: 'maven-publish'
+ apply plugin: "signing"
-static def authHeader() {
- def user = System.getenv('GITHUB_USER')
- if (!user) {
- throw new GradleException("Missing GITHUB_USER environment variable")
- }
- def token = System.getenv('GITHUB_TOKEN')
- if (!token) {
- throw new GradleException("Missing GITHUB_TOKEN environment variable")
- }
- def lp = "$user:$token"
- def encoded = Base64.getEncoder().encodeToString(lp.getBytes(StandardCharsets.UTF_8))
- return "Basic $encoded"
-}
-
-task cmakeNativeRuntime {
- doLast {
- mkdir "$buildDir/cpp"
- exec {
- workingDir "$buildDir/cpp"
- commandLine 'cmake', "-B", ".", "-S","$projectDir/cpp/", "-G", "Ninja"
- }
- }
-}
-
-task configureICU {
- onlyIf { !System.getenv('ICU_ROOT_DIR') }
- doLast {
- def os = osName()
- if (!file("$projectDir/external/icu/icu4c/source").exists()) {
- throw new GradleException("ICU submodule not detected. Please run `git submodule update --init`")
- }
- if (file("$projectDir/external/icu/icu4c/source/Makefile").exists()) {
- println("ICU Makefile detected, skipping ICU configure")
- } else {
- exec {
- workingDir "$projectDir/external/icu/icu4c/source"
- if (os.contains("linux")) {
- environment "CFLAGS", "-fPIC"
- environment "CXXFLAGS", "-fPIC"
- commandLine './runConfigureICU', 'Linux', '--enable-static', '--disable-shared'
- } else if (os.contains("mac")) {
- commandLine './runConfigureICU', 'MacOSX', '--enable-static', '--disable-shared'
- } else if (os.contains("win")) {
- commandLine 'sh', './runConfigureICU', 'MinGW', '--enable-static', '--disable-shared'
- } else {
- println("ICU configure not supported for OS '${System.getProperty("os.name")}'")
- }
- }
- }
- }
-}
+ publishing {
+ publications {
+ nativeRuntimeDist(MavenPublication) {
+ artifact System.env["NATIVERUNTIME_DIST_COMPAT_JAR"]
+ artifactId 'nativeruntime-dist-compat'
+ version System.env["NATIVERUNTIME_DIST_COMPAT_VERSION"]
-task buildICU {
- onlyIf { !System.getenv('ICU_ROOT_DIR') }
- dependsOn configureICU
- doLast {
- exec {
- def os = osName()
- if (os.contains("linux") || os.contains("mac") || os.contains("win")) {
- workingDir "$projectDir/external/icu/icu4c/source"
- commandLine 'make', '-j4'
- }
- }
- }
-}
+ pom {
+ name = "Robolectric Nativeruntime Distribution Compat"
+ description = "Robolectric Nativeruntime Distribution Compat"
+ url = "https://source.android.com/"
+ inceptionYear = "2008"
+ licenses {
+ license {
+ name = "Apache 2.0"
+ url = "http://www.apache.org/licenses/LICENSE-2.0"
+ comments = "While the EULA for the Android SDK restricts distribution of those binaries, the source code is licensed under Apache 2.0 which allows compiling binaries from source and then distributing those versions."
+ distribution = "repo"
+ }
+ }
-task makeNativeRuntime {
- dependsOn buildICU
- dependsOn cmakeNativeRuntime
- doLast {
- exec {
- workingDir "$buildDir/cpp"
- commandLine 'ninja'
- }
- }
-}
+ scm {
+ url = "https://android.googlesource.com/platform/manifest.git"
+ connection = "https://android.googlesource.com/platform/manifest.git"
+ }
-task copyNativeRuntimeToResources {
- def os = osName()
- if (System.getenv('SKIP_NATIVERUNTIME_BUILD')) {
- println("Skipping the nativeruntime build");
- } else if (!os.contains("linux") && !os.contains("mac") && !os.contains("win")) {
- println("Building the nativeruntime not supported for OS '${System.getProperty("os.name")}'")
- } else {
- dependsOn makeNativeRuntime
- outputs.dir "$buildDir/resources/main/native"
- doLast {
- copy {
- from ("$buildDir/cpp")
- include '*libnativeruntime.*'
- rename { String fileName ->
- if (os.contains("win")) {
- fileName.replace("libnativeruntime", "robolectric-nativeruntime")
- } else {
- fileName.replace("libnativeruntime", "librobolectric-nativeruntime")
+ developers {
+ developer {
+ name = "The Android Open Source Projects"
+ }
}
}
- into "$buildDir/resources/main/native/$os/${arch()}/"
}
}
- }
-}
-
-task copyNativeRuntimeFromGithubAction {
- outputs.dir "$buildDir/resources/main/native"
- doLast {
- def checkRunId = System.getenv('NATIVERUNTIME_ACTION_RUN_ID')
- def artifactsUrl = "https://api.github.com/repos/robolectric/robolectric/actions/runs/$checkRunId/artifacts"
- def downloadDir = new File("$buildDir/robolectric-nativeruntime-artifacts-$checkRunId")
- downloadDir.mkdirs()
- new JsonSlurper().parseText(new URL(artifactsUrl).text).artifacts.each { artifact ->
- def f = new File(downloadDir, "${artifact.name}.zip")
- if (!f.exists()) {
- println("Fetching ${artifact.name}.zip to $f")
- def conn = (HttpURLConnection) new URL(artifact.archive_download_url).openConnection()
- conn.instanceFollowRedirects = true
- conn.setRequestProperty("Authorization", authHeader())
+ repositories {
+ maven {
+ url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
- f.withOutputStream { out ->
- conn.inputStream.with { inp ->
- out << inp
- inp.close()
- out.close()
- }
+ credentials {
+ username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin']
+ password = System.properties["sonatype-password"] ?: System.env['sonatypePassword']
}
}
- copy {
- from zipTree(f)
- include "librobolectric*"
- rename { String fileName ->
- fileName = fileName.replaceFirst("librobolectric.*dylib", "librobolectric-nativeruntime.dylib")
- return fileName.replaceFirst("librobolectric.*so", "librobolectric-nativeruntime.so")
- }
- def os = "linux"
- if (artifact.name.contains("-mac")) {
- os = "mac"
- }
- def arch = "x86_64"
- if (artifact.name.contains("-arm64")) {
- arch = "aarch64"
- }
- into "$buildDir/resources/main/native/$os/$arch/"
- }
}
}
-}
-processResources {
- if (System.getenv('NATIVERUNTIME_ACTION_RUN_ID')) {
- dependsOn copyNativeRuntimeFromGithubAction
- } else {
- dependsOn copyNativeRuntimeToResources
+ signing {
+ sign publishing.publications.nativeRuntimeDist
}
}
@@ -186,6 +66,8 @@ dependencies {
api project(":utils:reflector")
api "com.google.guava:guava:$guavaJREVersion"
+ implementation "org.robolectric:nativeruntime-dist-compat:1.0.0"
+
annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
compileOnly AndroidSdk.MAX_SDK.coordinates
diff --git a/nativeruntime/cpp/CMakeLists.txt b/nativeruntime/cpp/CMakeLists.txt
deleted file mode 100644
index ee5d03c2e..000000000
--- a/nativeruntime/cpp/CMakeLists.txt
+++ /dev/null
@@ -1,186 +0,0 @@
-cmake_minimum_required(VERSION 3.10)
-
-# This is needed to ensure that static libraries can be linked into shared libraries.
-set(CMAKE_POSITION_INDEPENDENT_CODE ON)
-
-# Some libutils headers require C++17
-set (CMAKE_CXX_STANDARD 17)
-
-project(nativeruntime)
-
-if (WIN32)
- if(NOT DEFINED ENV{JAVA_HOME})
- message(FATAL_ERROR "JAVA_HOME is required in Windows")
- endif()
- # find_package JNI is broken on Windows, manually include header files
- set(JNI_INCLUDE_DIRS "$ENV{JAVA_HOME}/include" "$ENV{JAVA_HOME}/include/win32")
-else()
- find_package(JNI REQUIRED)
-endif()
-
-set(ANDROID_SQLITE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../external/sqlite")
-
-if(NOT EXISTS "${ANDROID_SQLITE_DIR}/dist/sqlite3.c")
- message(FATAL_ERROR "SQLite submodule missing. Please run `git submodule update --init`.")
-endif()
-
-if(DEFINED ENV{ICU_ROOT_DIR})
- if (WIN32)
- if(NOT EXISTS "$ENV{ICU_ROOT_DIR}/lib/libsicuin.a")
- message(FATAL_ERROR "ICU_ROOT_DIR does not contain 'lib/libsicuin.a'.")
- endif()
- else()
- if(NOT EXISTS "$ENV{ICU_ROOT_DIR}/lib/libicui18n.a")
- message(FATAL_ERROR "ICU_ROOT_DIR does not contain 'lib/libicui18n.a'.")
- endif()
- endif()
-
- message(NOTICE "Using $ENV{ICU_ROOT_DIR} as the ICU root dir")
- list(APPEND CMAKE_PREFIX_PATH "$ENV{ICU_ROOT_DIR}")
- if (WIN32)
- find_library(STATIC_ICUI18N_LIBRARY libsicuin.a)
- find_library(STATIC_ICUUC_LIBRARY libsicuuc.a)
- find_library(STATIC_ICUDATA_LIBRARY libsicudt.a)
- else()
- find_library(STATIC_ICUI18N_LIBRARY libicui18n.a)
- find_library(STATIC_ICUUC_LIBRARY libicuuc.a)
- find_library(STATIC_ICUDATA_LIBRARY libicudata.a)
- endif()
- include_directories($ENV{ICU_ROOT_DIR}/include)
-else()
- set(ICU_SUBMODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../external/icu")
-
- if(NOT EXISTS "${ICU_SUBMODULE_DIR}/icu4c/source/i18n/ucol.cpp")
- message(FATAL_ERROR "ICU submodule missing. Please run `git submodule update --init`.")
- endif()
-
- message(NOTICE "Using ${ICU_SUBMODULE_DIR} as the ICU root dir")
-
- if (WIN32)
- if(NOT EXISTS "${ICU_SUBMODULE_DIR}/icu4c/source/lib/libsicuin.a")
- message(FATAL_ERROR "ICU not built. Please run `./gradlew :nativeruntime:buildICU`.")
- endif()
- else()
- if(NOT EXISTS "${ICU_SUBMODULE_DIR}/icu4c/source/lib/libicui18n.a")
- message(FATAL_ERROR "ICU not built. Please run `./gradlew :nativeruntime:buildICU`.")
- endif()
- endif()
-
- list(APPEND CMAKE_PREFIX_PATH "${ICU_SUBMODULE_DIR}/icu4c/source/")
- if (WIN32)
- find_library(STATIC_ICUI18N_LIBRARY libsicuin.a)
- find_library(STATIC_ICUUC_LIBRARY libsicuuc.a)
- find_library(STATIC_ICUDATA_LIBRARY libsicudt.a)
- else()
- find_library(STATIC_ICUI18N_LIBRARY libicui18n.a)
- find_library(STATIC_ICUUC_LIBRARY libicuuc.a)
- find_library(STATIC_ICUDATA_LIBRARY libicudata.a)
- endif()
- include_directories(${ICU_SUBMODULE_DIR}/icu4c/source/i18n)
- include_directories(${ICU_SUBMODULE_DIR}/icu4c/source/common)
-endif()
-
-# Build flags derived from
-# https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:external/sqlite/dist/Android.bp
-
-set(SQLITE_COMPILE_OPTIONS
- -DHAVE_USLEEP=1
- -DNDEBUG=1
- -DSQLITE_DEFAULT_AUTOVACUUM=1
- -DSQLITE_DEFAULT_FILE_FORMAT=4
- -DSQLITE_DEFAULT_FILE_PERMISSIONS=0600
- -DSQLITE_DEFAULT_JOURNAL_SIZE_LIMIT=1048576
- -DSQLITE_DEFAULT_LEGACY_ALTER_TABLE
- -DSQLITE_ENABLE_BATCH_ATOMIC_WRITE
- -DSQLITE_ENABLE_FTS3
- -DSQLITE_ENABLE_FTS3=1
- -DSQLITE_ENABLE_FTS3_BACKWARDS
- -DSQLITE_ENABLE_FTS4
- -DSQLITE_ENABLE_ICU=1
- -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1
- -DSQLITE_HAVE_ISNAN
- -DSQLITE_OMIT_BUILTIN_TEST
- -DSQLITE_OMIT_COMPILEOPTION_DIAGS
- -DSQLITE_OMIT_LOAD_EXTENSION
- -DSQLITE_POWERSAFE_OVERWRITE=1
- -DSQLITE_SECURE_DELETE
- -DSQLITE_TEMP_STORE=3
- -DSQLITE_THREADSAFE=2
-)
-
-include_directories(${ANDROID_SQLITE_DIR}/dist)
-include_directories(${ANDROID_SQLITE_DIR}/android)
-
-add_library(androidsqlite STATIC
- ${ANDROID_SQLITE_DIR}/android/OldPhoneNumberUtils.cpp
- ${ANDROID_SQLITE_DIR}/android/PhoneNumberUtils.cpp
- ${ANDROID_SQLITE_DIR}/android/PhoneNumberUtils.h
- ${ANDROID_SQLITE_DIR}/android/sqlite3_android.cpp
- ${ANDROID_SQLITE_DIR}/dist/sqlite3.c
- ${ANDROID_SQLITE_DIR}/dist/sqlite3ext.h
-)
-
-target_compile_options(androidsqlite PRIVATE ${SQLITE_COMPILE_OPTIONS})
-
-if (WIN32)
- target_link_libraries(androidsqlite
- --static
- ${STATIC_ICUI18N_LIBRARY}
- ${STATIC_ICUUC_LIBRARY}
- ${STATIC_ICUDATA_LIBRARY}
- gcc
- stdc++
- )
-else()
- target_link_libraries(androidsqlite
- ${STATIC_ICUI18N_LIBRARY}
- ${STATIC_ICUUC_LIBRARY}
- ${STATIC_ICUDATA_LIBRARY}
- -ldl
- -lpthread
- )
-endif()
-
-include_directories(${JNI_INCLUDE_DIRS})
-
-add_subdirectory (liblog)
-include_directories(liblog/include)
-
-include_directories(libnativehelper/include)
-
-add_subdirectory (libutils)
-include_directories(libutils/include)
-
-add_subdirectory (androidfw)
-include_directories(androidfw/include)
-
-add_subdirectory (libcutils)
-include_directories(libcutils/include)
-
-include_directories(base/include)
-
-add_library(nativeruntime SHARED
- jni/AndroidRuntime.cpp
- jni/AndroidRuntime.h
- jni/JNIMain.cpp
- jni/robo_android_database_CursorWindow.cpp
- jni/robo_android_database_SQLiteCommon.cpp
- jni/robo_android_database_SQLiteCommon.h
- jni/robo_android_database_SQLiteConnection.cpp
-)
-
-target_link_libraries(nativeruntime
- log
- utils
- androidsqlite
- cutils
- androidfw
-)
-
-if (CMAKE_HOST_SYSTEM_NAME MATCHES "Linux")
- target_link_libraries(nativeruntime
- -static-libgcc
- -static-libstdc++
- -Wl,--no-undefined # print an error if there are any undefined symbols
- )
-endif()
diff --git a/nativeruntime/cpp/androidfw/CMakeLists.txt b/nativeruntime/cpp/androidfw/CMakeLists.txt
deleted file mode 100644
index 87b89dde5..000000000
--- a/nativeruntime/cpp/androidfw/CMakeLists.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-cmake_minimum_required(VERSION 3.10)
-
-project(androidfw)
-
-include_directories(include)
-
-include_directories(../libutils/include)
-include_directories(../libcutils/include)
-include_directories(../liblog/include)
-
-add_library(androidfw STATIC CursorWindow.cpp)
diff --git a/nativeruntime/cpp/androidfw/CursorWindow.cpp b/nativeruntime/cpp/androidfw/CursorWindow.cpp
deleted file mode 100644
index 59b767aa7..000000000
--- a/nativeruntime/cpp/androidfw/CursorWindow.cpp
+++ /dev/null
@@ -1,431 +0,0 @@
-/*
- * Copyright (C) 2006-2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/libs/androidfw/CursorWindow.cpp
-
-#undef LOG_TAG
-#define LOG_TAG "CursorWindow"
-
-#include <androidfw/CursorWindow.h>
-// #include <binder/Parcel.h>
-#include <assert.h>
-#include <cutils/ashmem.h>
-#include <log/log.h>
-#include <stdlib.h>
-#include <string.h>
-#if !defined(_WIN32)
-#include <sys/mman.h>
-#endif
-#include <unistd.h>
-
-namespace android {
-
-CursorWindow::CursorWindow(const String8& name, int ashmemFd, void* data,
- size_t size, bool readOnly)
- : mName(name),
- mAshmemFd(ashmemFd),
- mData(data),
- mSize(size),
- mReadOnly(readOnly) {
- #if defined(_WIN32)
- mHeader = new Header;
- #else
- mHeader = static_cast<Header*>(mData);
- #endif
-}
-
-CursorWindow::~CursorWindow() {
- #if defined(_WIN32)
- delete mHeader;
- #else
- ::munmap(mData, mSize);
- ::close(mAshmemFd);
- #endif
-}
-
-#if defined(_WIN32)
-status_t CursorWindow::create(const String8& name, size_t size,
- CursorWindow** outCursorWindow) {
- String8 ashmemName("CursorWindow: ");
- ashmemName.append(name);
-
- status_t result;
- // We don't use ashmem here, and CursorWindow constructor will use in-memory struct
- // to support Windows.
- CursorWindow* window = new CursorWindow(name, -1, nullptr, size, true /*readOnly*/);
- LOG_WINDOW("Created CursorWindow from parcel: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- window->mHeader->freeOffset,
- window->mHeader->numRows,
- window->mHeader->numColumns,
- window->mSize, window->mData);
- if (window != nullptr) {
- *outCursorWindow = window;
- return OK;
- }
- *outCursorWindow = nullptr;
- return result;
-}
-#else
-status_t CursorWindow::create(const String8& name, size_t size,
- CursorWindow** outCursorWindow) {
- String8 ashmemName("CursorWindow: ");
- ashmemName.append(name);
-
- status_t result;
- int ashmemFd = ashmem_create_region(ashmemName.string(), size);
- if (ashmemFd < 0) {
- result = -errno;
- ALOGE("CursorWindow: ashmem_create_region() failed: errno=%d.", errno);
- } else {
- result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE);
- if (result < 0) {
- ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d", errno);
- } else {
- void* data = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED,
- ashmemFd, 0);
- if (data == MAP_FAILED) {
- result = -errno;
- ALOGE("CursorWindow: mmap() failed: errno=%d.", errno);
- } else {
- result = ashmem_set_prot_region(ashmemFd, PROT_READ);
- if (result < 0) {
- ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d.",
- errno);
- } else {
- CursorWindow* window =
- new CursorWindow(name, ashmemFd, data, size, false /*readOnly*/);
- result = window->clear();
- if (!result) {
- LOG_WINDOW(
- "Created new CursorWindow: freeOffset=%d, "
- "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
- window->mHeader->freeOffset, window->mHeader->numRows,
- window->mHeader->numColumns, window->mSize, window->mData);
- *outCursorWindow = window;
- return OK;
- }
- delete window;
- }
- }
- ::munmap(data, size);
- }
- ::close(ashmemFd);
- }
- *outCursorWindow = nullptr;
- return result;
-}
-#endif
-//
-// status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow**
-// outCursorWindow) {
-// String8 name = parcel->readString8();
-//
-// status_t result;
-// int actualSize;
-// int ashmemFd = parcel->readFileDescriptor();
-// if (ashmemFd == int(BAD_TYPE)) {
-// result = BAD_TYPE;
-// ALOGE("CursorWindow: readFileDescriptor() failed");
-// } else {
-// ssize_t size = ashmem_get_size_region(ashmemFd);
-// if (size < 0) {
-// result = UNKNOWN_ERROR;
-// ALOGE("CursorWindow: ashmem_get_size_region() failed: errno=%d.",
-// errno);
-// } else {
-// int dupAshmemFd = ::fcntl(ashmemFd, F_DUPFD_CLOEXEC, 0);
-// if (dupAshmemFd < 0) {
-// result = -errno;
-// ALOGE("CursorWindow: fcntl() failed: errno=%d.", errno);
-// } else {
-// // the size of the ashmem descriptor can be modified between
-// ashmem_get_size_region
-// // call and mmap, so we'll check again immediately after memory is
-// mapped void* data = ::mmap(NULL, size, PROT_READ, MAP_SHARED,
-// dupAshmemFd, 0); if (data == MAP_FAILED) {
-// result = -errno;
-// ALOGE("CursorWindow: mmap() failed: errno=%d.", errno);
-// } else if ((actualSize = ashmem_get_size_region(dupAshmemFd)) != size)
-// {
-// ::munmap(data, size);
-// result = BAD_VALUE;
-// ALOGE("CursorWindow: ashmem_get_size_region() returned %d, expected
-// %d"
-// " errno=%d",
-// actualSize, (int) size, errno);
-// } else {
-// CursorWindow* window = new CursorWindow(name, dupAshmemFd,
-// data, size, true
-// /*readOnly*/);
-// LOG_WINDOW("Created CursorWindow from parcel: freeOffset=%d, "
-// "numRows=%d, numColumns=%d, mSize=%zu, mData=%p",
-// window->mHeader->freeOffset,
-// window->mHeader->numRows,
-// window->mHeader->numColumns,
-// window->mSize, window->mData);
-// *outCursorWindow = window;
-// return OK;
-// }
-// ::close(dupAshmemFd);
-// }
-// }
-// }
-// *outCursorWindow = NULL;
-// return result;
-//}
-//
-// status_t CursorWindow::writeToParcel(Parcel* parcel) {
-// status_t status = parcel->writeString8(mName);
-// if (!status) {
-// status = parcel->writeDupFileDescriptor(mAshmemFd);
-// }
-// return status;
-//}
-
-status_t CursorWindow::clear() {
- if (mReadOnly) {
- return INVALID_OPERATION;
- }
-
- mHeader->freeOffset = sizeof(Header) + sizeof(RowSlotChunk);
- mHeader->firstChunkOffset = sizeof(Header);
- mHeader->numRows = 0;
- mHeader->numColumns = 0;
-
- RowSlotChunk* firstChunk =
- static_cast<RowSlotChunk*>(offsetToPtr(mHeader->firstChunkOffset));
- firstChunk->nextChunkOffset = 0;
- return OK;
-}
-
-status_t CursorWindow::setNumColumns(uint32_t numColumns) {
- if (mReadOnly) {
- return INVALID_OPERATION;
- }
-
- uint32_t cur = mHeader->numColumns;
- if ((cur > 0 || mHeader->numRows > 0) && cur != numColumns) {
- ALOGE("Trying to go from %d columns to %d", cur, numColumns);
- return INVALID_OPERATION;
- }
- mHeader->numColumns = numColumns;
- return OK;
-}
-
-status_t CursorWindow::allocRow() {
- if (mReadOnly) {
- return INVALID_OPERATION;
- }
-
- // Fill in the row slot
- RowSlot* rowSlot = allocRowSlot();
- if (rowSlot == nullptr) {
- return NO_MEMORY;
- }
-
- // Allocate the slots for the field directory
- size_t fieldDirSize = mHeader->numColumns * sizeof(FieldSlot);
- uint32_t fieldDirOffset = alloc(fieldDirSize, true /*aligned*/);
- if (!fieldDirOffset) {
- mHeader->numRows--;
- LOG_WINDOW(
- "The row failed, so back out the new row accounting "
- "from allocRowSlot %d",
- mHeader->numRows);
- return NO_MEMORY;
- }
- FieldSlot* fieldDir = static_cast<FieldSlot*>(offsetToPtr(fieldDirOffset));
- memset(fieldDir, 0, fieldDirSize);
-
- LOG_WINDOW(
- "Allocated row %u, rowSlot is at offset %u, fieldDir is %zu bytes at "
- "offset %u\n",
- mHeader->numRows - 1, offsetFromPtr(rowSlot), fieldDirSize,
- fieldDirOffset);
- rowSlot->offset = fieldDirOffset;
- return OK;
-}
-
-status_t CursorWindow::freeLastRow() {
- if (mReadOnly) {
- return INVALID_OPERATION;
- }
-
- if (mHeader->numRows > 0) {
- mHeader->numRows--;
- }
- return OK;
-}
-
-uint32_t CursorWindow::alloc(size_t size, bool aligned) {
- uint32_t padding;
- if (aligned) {
- // 4 byte alignment
- padding = (~mHeader->freeOffset + 1) & 3;
- } else {
- padding = 0;
- }
-
- uint32_t offset = mHeader->freeOffset + padding;
- uint32_t nextFreeOffset = offset + size;
- if (nextFreeOffset > mSize) {
- // ALOGW("Window is full: requested allocation %zu bytes, "
- // "free space %zu bytes, window size %zu bytes",
- // size, freeSpace(), mSize);
- return 0;
- }
-
- mHeader->freeOffset = nextFreeOffset;
- return offset;
-}
-
-CursorWindow::RowSlot* CursorWindow::getRowSlot(uint32_t row) {
- uint32_t chunkPos = row;
- RowSlotChunk* chunk =
- static_cast<RowSlotChunk*>(offsetToPtr(mHeader->firstChunkOffset));
- while (chunkPos >= ROW_SLOT_CHUNK_NUM_ROWS) {
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS;
- }
- return &chunk->slots[chunkPos];
-}
-
-CursorWindow::RowSlot* CursorWindow::allocRowSlot() {
- uint32_t chunkPos = mHeader->numRows;
- RowSlotChunk* chunk =
- static_cast<RowSlotChunk*>(offsetToPtr(mHeader->firstChunkOffset));
- while (chunkPos > ROW_SLOT_CHUNK_NUM_ROWS) {
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS;
- }
- if (chunkPos == ROW_SLOT_CHUNK_NUM_ROWS) {
- if (!chunk->nextChunkOffset) {
- chunk->nextChunkOffset = alloc(sizeof(RowSlotChunk), true /*aligned*/);
- if (!chunk->nextChunkOffset) {
- return nullptr;
- }
- }
- chunk = static_cast<RowSlotChunk*>(offsetToPtr(chunk->nextChunkOffset));
- chunk->nextChunkOffset = 0;
- chunkPos = 0;
- }
- mHeader->numRows += 1;
- return &chunk->slots[chunkPos];
-}
-
-CursorWindow::FieldSlot* CursorWindow::getFieldSlot(uint32_t row,
- uint32_t column) {
- if (row >= mHeader->numRows || column >= mHeader->numColumns) {
- ALOGE(
- "Failed to read row %d, column %d from a CursorWindow which "
- "has %d rows, %d columns.",
- row, column, mHeader->numRows, mHeader->numColumns);
- return nullptr;
- }
- RowSlot* rowSlot = getRowSlot(row);
- if (!rowSlot) {
- ALOGE("Failed to find rowSlot for row %d.", row);
- return nullptr;
- }
- FieldSlot* fieldDir = static_cast<FieldSlot*>(offsetToPtr(rowSlot->offset));
- return &fieldDir[column];
-}
-
-status_t CursorWindow::putBlob(uint32_t row, uint32_t column, const void* value,
- size_t size) {
- return putBlobOrString(row, column, value, size, FIELD_TYPE_BLOB);
-}
-
-status_t CursorWindow::putString(uint32_t row, uint32_t column,
- const char* value, size_t sizeIncludingNull) {
- return putBlobOrString(row, column, value, sizeIncludingNull,
- FIELD_TYPE_STRING);
-}
-
-status_t CursorWindow::putBlobOrString(uint32_t row, uint32_t column,
- const void* value, size_t size,
- int32_t type) {
- if (mReadOnly) {
- return INVALID_OPERATION;
- }
-
- FieldSlot* fieldSlot = getFieldSlot(row, column);
- if (!fieldSlot) {
- return BAD_VALUE;
- }
-
- uint32_t offset = alloc(size);
- if (!offset) {
- return NO_MEMORY;
- }
-
- memcpy(offsetToPtr(offset), value, size);
-
- fieldSlot->type = type;
- fieldSlot->data.buffer.offset = offset;
- fieldSlot->data.buffer.size = size;
- return OK;
-}
-
-status_t CursorWindow::putLong(uint32_t row, uint32_t column, int64_t value) {
- if (mReadOnly) {
- return INVALID_OPERATION;
- }
-
- FieldSlot* fieldSlot = getFieldSlot(row, column);
- if (!fieldSlot) {
- return BAD_VALUE;
- }
-
- fieldSlot->type = FIELD_TYPE_INTEGER;
- fieldSlot->data.l = value;
- return OK;
-}
-
-status_t CursorWindow::putDouble(uint32_t row, uint32_t column, double value) {
- if (mReadOnly) {
- return INVALID_OPERATION;
- }
-
- FieldSlot* fieldSlot = getFieldSlot(row, column);
- if (!fieldSlot) {
- return BAD_VALUE;
- }
-
- fieldSlot->type = FIELD_TYPE_FLOAT;
- fieldSlot->data.d = value;
- return OK;
-}
-
-status_t CursorWindow::putNull(uint32_t row, uint32_t column) {
- if (mReadOnly) {
- return INVALID_OPERATION;
- }
-
- FieldSlot* fieldSlot = getFieldSlot(row, column);
- if (!fieldSlot) {
- return BAD_VALUE;
- }
-
- fieldSlot->type = FIELD_TYPE_NULL;
- fieldSlot->data.buffer.offset = 0;
- fieldSlot->data.buffer.size = 0;
- return OK;
-}
-
-}; // namespace android
diff --git a/nativeruntime/cpp/androidfw/include/androidfw/CursorWindow.h b/nativeruntime/cpp/androidfw/include/androidfw/CursorWindow.h
deleted file mode 100644
index 8975c648b..000000000
--- a/nativeruntime/cpp/androidfw/include/androidfw/CursorWindow.h
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright (C) 2006 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/libs/androidfw/include/androidfw/CursorWindow.h
-
-#ifndef _ANDROID__DATABASE_WINDOW_H
-#define _ANDROID__DATABASE_WINDOW_H
-
-#include <inttypes.h>
-#include <stddef.h>
-#include <stdint.h>
-
-// #include <binder/Parcel.h>
-#include <log/log.h>
-#include <utils/String8.h>
-
-#if LOG_NDEBUG
-
-#define IF_LOG_WINDOW() if (false)
-#define LOG_WINDOW(...)
-
-#else
-
-#define IF_LOG_WINDOW() IF_ALOG(LOG_DEBUG, "CursorWindow")
-#define LOG_WINDOW(...) ALOG(LOG_DEBUG, "CursorWindow", __VA_ARGS__)
-
-#endif
-
-namespace android {
-
-/**
- * This class stores a set of rows from a database in a buffer. The beginning of
- * the window has first chunk of RowSlots, which are offsets to the row
- * directory, followed by an offset to the next chunk in a linked-list of
- * additional chunk of RowSlots in case the pre-allocated chunk isn't big enough
- * to refer to all rows. Each row directory has a FieldSlot per column, which
- * has the size, offset, and type of the data for that field. Note that the data
- * types come from sqlite3.h.
- *
- * Strings are stored in UTF-8.
- */
-class CursorWindow {
- CursorWindow(const String8& name, int ashmemFd, void* data, size_t size,
- bool readOnly);
-
- public:
- /* Field types. */
- enum {
- FIELD_TYPE_NULL = 0,
- FIELD_TYPE_INTEGER = 1,
- FIELD_TYPE_FLOAT = 2,
- FIELD_TYPE_STRING = 3,
- FIELD_TYPE_BLOB = 4,
- };
-
- /* Opaque type that describes a field slot. */
- struct FieldSlot {
- private:
- int32_t type;
- union {
- double d;
- int64_t l;
- struct {
- uint32_t offset;
- uint32_t size;
- } buffer;
- } data;
-
- friend class CursorWindow;
- } __attribute((packed));
-
- ~CursorWindow();
-
- static status_t create(const String8& name, size_t size,
- CursorWindow** outCursorWindow);
- // static status_t createFromParcel(Parcel* parcel, CursorWindow**
- // outCursorWindow);
- //
- // status_t writeToParcel(Parcel* parcel);
-
- inline String8 name() { return mName; }
- inline size_t size() { return mSize; }
- inline size_t freeSpace() { return mSize - mHeader->freeOffset; }
- inline uint32_t getNumRows() { return mHeader->numRows; }
- inline uint32_t getNumColumns() { return mHeader->numColumns; }
-
- status_t clear();
- status_t setNumColumns(uint32_t numColumns);
-
- /**
- * Allocate a row slot and its directory.
- * The row is initialized will null entries for each field.
- */
- status_t allocRow();
- status_t freeLastRow();
-
- status_t putBlob(uint32_t row, uint32_t column, const void* value,
- size_t size);
- status_t putString(uint32_t row, uint32_t column, const char* value,
- size_t sizeIncludingNull);
- status_t putLong(uint32_t row, uint32_t column, int64_t value);
- status_t putDouble(uint32_t row, uint32_t column, double value);
- status_t putNull(uint32_t row, uint32_t column);
-
- /**
- * Gets the field slot at the specified row and column.
- * Returns null if the requested row or column is not in the window.
- */
- FieldSlot* getFieldSlot(uint32_t row, uint32_t column);
-
- inline int32_t getFieldSlotType(FieldSlot* fieldSlot) {
- return fieldSlot->type;
- }
-
- inline int64_t getFieldSlotValueLong(FieldSlot* fieldSlot) {
- return fieldSlot->data.l;
- }
-
- inline double getFieldSlotValueDouble(FieldSlot* fieldSlot) {
- return fieldSlot->data.d;
- }
-
- inline const char* getFieldSlotValueString(FieldSlot* fieldSlot,
- size_t* outSizeIncludingNull) {
- *outSizeIncludingNull = fieldSlot->data.buffer.size;
- return static_cast<char*>(offsetToPtr(fieldSlot->data.buffer.offset,
- fieldSlot->data.buffer.size));
- }
-
- inline const void* getFieldSlotValueBlob(FieldSlot* fieldSlot,
- size_t* outSize) {
- *outSize = fieldSlot->data.buffer.size;
- return offsetToPtr(fieldSlot->data.buffer.offset,
- fieldSlot->data.buffer.size);
- }
-
- private:
- static const size_t ROW_SLOT_CHUNK_NUM_ROWS = 100;
-
- struct Header {
- // Offset of the lowest unused byte in the window.
- uint32_t freeOffset;
-
- // Offset of the first row slot chunk.
- uint32_t firstChunkOffset;
-
- uint32_t numRows;
- uint32_t numColumns;
- };
-
- struct RowSlot {
- uint32_t offset;
- };
-
- struct RowSlotChunk {
- RowSlot slots[ROW_SLOT_CHUNK_NUM_ROWS];
- uint32_t nextChunkOffset;
- };
-
- String8 mName;
- int mAshmemFd;
- void* mData;
- size_t mSize;
- bool mReadOnly;
- Header* mHeader;
-
- inline void* offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) {
- if (offset >= mSize) {
- // ALOGE("Offset %" PRIu32 " out of bounds, max value %zu", offset,
- // mSize);
- return nullptr;
- }
- if (offset + bufferSize > mSize) {
- // ALOGE("End offset %" PRIu32 " out of bounds, max value %zu",
- // offset + bufferSize, mSize);
- return nullptr;
- }
- return static_cast<uint8_t*>(mData) + offset;
- }
-
- inline uint32_t offsetFromPtr(void* ptr) {
- return static_cast<uint8_t*>(ptr) - static_cast<uint8_t*>(mData);
- }
-
- /**
- * Allocate a portion of the window. Returns the offset
- * of the allocation, or 0 if there isn't enough space.
- * If aligned is true, the allocation gets 4 byte alignment.
- */
- uint32_t alloc(size_t size, bool aligned = false);
-
- RowSlot* getRowSlot(uint32_t row);
- RowSlot* allocRowSlot();
-
- status_t putBlobOrString(uint32_t row, uint32_t column, const void* value,
- size_t size, int32_t type);
-};
-
-}; // namespace android
-
-#endif
diff --git a/nativeruntime/cpp/base/include/android-base/macros.h b/nativeruntime/cpp/base/include/android-base/macros.h
deleted file mode 100644
index 5a4a13ade..000000000
--- a/nativeruntime/cpp/base/include/android-base/macros.h
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/base/include/android-base/macros.h
-
-#ifndef UTILS_MACROS_H
-#define UTILS_MACROS_H
-
-#include <stddef.h> // for size_t
-#include <unistd.h> // for TEMP_FAILURE_RETRY
-
-#include <utility>
-
-// bionic and glibc both have TEMP_FAILURE_RETRY, but eg Mac OS' libc doesn't.
-#ifndef TEMP_FAILURE_RETRY
-#define TEMP_FAILURE_RETRY(exp) \
- ({ \
- decltype(exp) _rc; \
- do { \
- _rc = (exp); \
- } while (_rc == -1 && errno == EINTR); \
- _rc; \
- })
-#endif
-
-// A macro to disallow the copy constructor and operator= functions
-// This must be placed in the private: declarations for a class.
-//
-// For disallowing only assign or copy, delete the relevant operator or
-// constructor, for example:
-// void operator=(const TypeName&) = delete;
-// Note, that most uses of DISALLOW_ASSIGN and DISALLOW_COPY are broken
-// semantically, one should either use disallow both or neither. Try to
-// avoid these in new code.
-#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
- TypeName(const TypeName&) = delete; \
- void operator=(const TypeName&) = delete
-
-// A macro to disallow all the implicit constructors, namely the
-// default constructor, copy constructor and operator= functions.
-//
-// This should be used in the private: declarations for a class
-// that wants to prevent anyone from instantiating it. This is
-// especially useful for classes containing only static methods.
-#define DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \
- TypeName() = delete; \
- DISALLOW_COPY_AND_ASSIGN(TypeName)
-
-// The arraysize(arr) macro returns the # of elements in an array arr.
-// The expression is a compile-time constant, and therefore can be
-// used in defining new arrays, for example. If you use arraysize on
-// a pointer by mistake, you will get a compile-time error.
-//
-// One caveat is that arraysize() doesn't accept any array of an
-// anonymous type or a type defined inside a function. In these rare
-// cases, you have to use the unsafe ARRAYSIZE_UNSAFE() macro below. This is
-// due to a limitation in C++'s template system. The limitation might
-// eventually be removed, but it hasn't happened yet.
-
-// This template function declaration is used in defining arraysize.
-// Note that the function doesn't need an implementation, as we only
-// use its type.
-template <typename T, size_t N>
-char (&ArraySizeHelper(T (&array)[N]))[N]; // NOLINT(readability/casting)
-
-#define arraysize(array) (sizeof(ArraySizeHelper(array)))
-
-#define SIZEOF_MEMBER(t, f) sizeof(std::declval<t>().f)
-
-// Changing this definition will cause you a lot of pain. A majority of
-// vendor code defines LIKELY and UNLIKELY this way, and includes
-// this header through an indirect path.
-#define LIKELY(exp) (__builtin_expect((exp) != 0, true))
-#define UNLIKELY(exp) (__builtin_expect((exp) != 0, false))
-
-#define WARN_UNUSED __attribute__((warn_unused_result))
-
-// A deprecated function to call to create a false use of the parameter, for
-// example:
-// int foo(int x) { UNUSED(x); return 10; }
-// to avoid compiler warnings. Going forward we prefer ATTRIBUTE_UNUSED.
-template <typename... T>
-void UNUSED(const T&...) {}
-
-// An attribute to place on a parameter to a function, for example:
-// int foo(int x ATTRIBUTE_UNUSED) { return 10; }
-// to avoid compiler warnings.
-#define ATTRIBUTE_UNUSED __attribute__((__unused__))
-
-// The FALLTHROUGH_INTENDED macro can be used to annotate implicit fall-through
-// between switch labels:
-// switch (x) {
-// case 40:
-// case 41:
-// if (truth_is_out_there) {
-// ++x;
-// FALLTHROUGH_INTENDED; // Use instead of/along with annotations in
-// // comments.
-// } else {
-// return x;
-// }
-// case 42:
-// ...
-//
-// As shown in the example above, the FALLTHROUGH_INTENDED macro should be
-// followed by a semicolon. It is designed to mimic control-flow statements
-// like 'break;', so it can be placed in most places where 'break;' can, but
-// only if there are no statements on the execution path between it and the
-// next switch label.
-//
-// When compiled with clang, the FALLTHROUGH_INTENDED macro is expanded to
-// [[clang::fallthrough]] attribute, which is analysed when performing switch
-// labels fall-through diagnostic ('-Wimplicit-fallthrough'). See clang
-// documentation on language extensions for details:
-// http://clang.llvm.org/docs/LanguageExtensions.html#clang__fallthrough
-//
-// When used with unsupported compilers, the FALLTHROUGH_INTENDED macro has no
-// effect on diagnostics.
-//
-// In either case this macro has no effect on runtime behavior and performance
-// of code.
-#ifndef FALLTHROUGH_INTENDED
-#define FALLTHROUGH_INTENDED [[clang::fallthrough]] // NOLINT
-#endif
-
-// Current ABI string
-#if defined(__arm__)
-#define ABI_STRING "arm"
-#elif defined(__aarch64__)
-#define ABI_STRING "arm64"
-#elif defined(__i386__)
-#define ABI_STRING "x86"
-#elif defined(__x86_64__)
-#define ABI_STRING "x86_64"
-#elif defined(__mips__) && !defined(__LP64__)
-#define ABI_STRING "mips"
-#elif defined(__mips__) && defined(__LP64__)
-#define ABI_STRING "mips64"
-#endif
-
-#endif // UTILS_MACROS_H
diff --git a/nativeruntime/cpp/jni/AndroidRuntime.cpp b/nativeruntime/cpp/jni/AndroidRuntime.cpp
deleted file mode 100644
index e22381c17..000000000
--- a/nativeruntime/cpp/jni/AndroidRuntime.cpp
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/core/jni/AndroidRuntime.cpp
-
-#include "AndroidRuntime.h"
-
-#include <assert.h>
-
-#include "jni.h"
-
-using namespace android;
-
-/*static*/ JavaVM* AndroidRuntime::mJavaVM = nullptr;
-
-/*static*/ JavaVM* AndroidRuntime::getJavaVM() {
- return AndroidRuntime::mJavaVM;
-}
-
-/*
- * Get the JNIEnv pointer for this thread.
- *
- * Returns NULL if the slot wasn't allocated or populated.
- */
-/*static*/ JNIEnv* AndroidRuntime::getJNIEnv() {
- JNIEnv* env;
- JavaVM* vm = AndroidRuntime::getJavaVM();
- assert(vm != nullptr);
-
- if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_4) != JNI_OK)
- return nullptr;
- return env;
-}
diff --git a/nativeruntime/cpp/jni/AndroidRuntime.h b/nativeruntime/cpp/jni/AndroidRuntime.h
deleted file mode 100644
index 5e1d47ea9..000000000
--- a/nativeruntime/cpp/jni/AndroidRuntime.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/core/jni/include/android_runtime/AndroidRuntime.h
-
-#ifndef _RUNTIME_ANDROID_RUNTIME_H
-#define _RUNTIME_ANDROID_RUNTIME_H
-
-#include <jni.h>
-
-namespace android {
-
-class AndroidRuntime {
- public:
- /** return a pointer to the VM running in this process */
- static JavaVM* getJavaVM();
-
- /** return a pointer to the JNIEnv pointer for this thread */
- static JNIEnv* getJNIEnv();
-
- private:
- /* JNI JavaVM pointer */
- static JavaVM* mJavaVM;
-};
-} // namespace android
-
-#endif
diff --git a/nativeruntime/cpp/jni/JNIMain.cpp b/nativeruntime/cpp/jni/JNIMain.cpp
deleted file mode 100644
index 166e89474..000000000
--- a/nativeruntime/cpp/jni/JNIMain.cpp
+++ /dev/null
@@ -1,62 +0,0 @@
-#include <jni.h>
-#include <log/log.h>
-
-#include "unicode/locid.h"
-
-namespace android {
-
-extern int register_android_database_CursorWindow(JNIEnv* env);
-extern int register_android_database_SQLiteConnection(JNIEnv* env);
-
-} // namespace android
-
-/*
- * JNI Initialization
- */
-jint JNI_OnLoad(JavaVM* jvm, void* reserved) {
- JNIEnv* env;
-
- ALOGV("loading JNI\n");
- // Check JNI version
- if (jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_4)) {
- ALOGE("JNI version mismatch error");
- return JNI_ERR;
- }
-
- if (android::register_android_database_CursorWindow(env) != JNI_VERSION_1_4 ||
- android::register_android_database_SQLiteConnection(env) !=
- JNI_VERSION_1_4) {
- ALOGE("Failure during registration");
- return JNI_ERR;
- }
-
- // Configuration is stored as java System properties.
- // Get a reference to System.getProperty
- jclass systemClass = env->FindClass("java/lang/System");
- jmethodID getPropertyMethod = env->GetStaticMethodID(
- systemClass, "getProperty",
- "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
-
- // Set the default locale, which is required for e.g. SQLite's 'COLLATE
- // UNICODE'.
- auto stringLanguageTag = (jstring)env->CallStaticObjectMethod(
- systemClass, getPropertyMethod,
- env->NewStringUTF("robolectric.nativeruntime.languageTag"),
- env->NewStringUTF(""));
- const char* languageTag = env->GetStringUTFChars(stringLanguageTag, 0);
- int languageTagLength = env->GetStringLength(stringLanguageTag);
- if (languageTagLength > 0) {
- UErrorCode status = U_ZERO_ERROR;
- icu::Locale locale = icu::Locale::forLanguageTag(languageTag, status);
- if (U_SUCCESS(status)) {
- icu::Locale::setDefault(locale, status);
- }
- if (U_FAILURE(status)) {
- fprintf(stderr,
- "Failed to set the ICU default locale to '%s' (error code %d)\n",
- languageTag, status);
- }
- }
- env->ReleaseStringUTFChars(stringLanguageTag, languageTag);
- return JNI_VERSION_1_4;
-}
diff --git a/nativeruntime/cpp/jni/robo_android_database_CursorWindow.cpp b/nativeruntime/cpp/jni/robo_android_database_CursorWindow.cpp
deleted file mode 100644
index 9481b8e41..000000000
--- a/nativeruntime/cpp/jni/robo_android_database_CursorWindow.cpp
+++ /dev/null
@@ -1,628 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/core/jni/android_database_CursorWindow.cpp
-
-#undef LOG_TAG
-#define LOG_TAG "CursorWindow"
-#define LOG_NDEBUG 0
-
-#include <dirent.h>
-#include <inttypes.h>
-#include <jni.h>
-#include <log/log.h>
-#include <nativehelper/JNIHelp.h>
-#include <stdio.h>
-#include <string.h>
-#include <sys/types.h>
-#include <unistd.h>
-#include <utils/String16.h>
-#include <utils/String8.h>
-#include <utils/Unicode.h>
-
-#undef LOG_NDEBUG
-#define LOG_NDEBUG 1
-
-#include <androidfw/CursorWindow.h>
-// #include "android_os_Parcel.h"
-// #include "android_util_Binder.h"
-#include <nativehelper/scoped_local_ref.h>
-
-#include "robo_android_database_SQLiteCommon.h"
-
-namespace android {
-
-// static struct {
-// jfieldID data;
-// jfieldID sizeCopied;
-// } gCharArrayBufferClassInfo;
-
-static jfieldID getCharArrayBufferDataFieldId(JNIEnv* env, jobject obj) {
- jclass clsObj = env->GetObjectClass(obj);
- if (clsObj == nullptr) {
- printf("cls obj is null");
- }
- return env->GetFieldID(clsObj, "data", "[C");
-}
-
-static jfieldID getCharArrayBufferSizeCopiedFieldId(JNIEnv* env, jobject obj) {
- jclass clsObj = env->GetObjectClass(obj);
- if (clsObj == nullptr) {
- printf("cls obj is null");
- }
- return env->GetFieldID(clsObj, "sizeCopied", "I");
-}
-
-static jstring gEmptyString;
-
-static void throwExceptionWithRowCol(JNIEnv* env, jint row, jint column) {
- String8 msg;
- msg.appendFormat(
- "Couldn't read row %d, col %d from CursorWindow. "
- "Make sure the Cursor is initialized correctly before accessing data "
- "from it.",
- row, column);
- jniThrowException(env, "java/lang/IllegalStateException", msg.string());
-}
-
-static void throwUnknownTypeException(JNIEnv* env, jint type) {
- String8 msg;
- msg.appendFormat("UNKNOWN type %d", type);
- jniThrowException(env, "java/lang/IllegalStateException", msg.string());
-}
-
-// static int getFdCount() {
-// char fdpath[PATH_MAX];
-// int count = 0;
-// snprintf(fdpath, PATH_MAX, "/proc/%d/fd", getpid());
-// DIR* dir = opendir(fdpath);
-// if (dir != NULL) {
-// struct dirent* dirent;
-// while ((dirent = readdir(dir))) {
-// count++;
-// }
-// count -= 2; // discount "." and ".."
-// closedir(dir);
-// }
-// return count;
-// }
-
-static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring nameObj,
- jint cursorWindowSize) {
- String8 name;
- const char* nameStr = env->GetStringUTFChars(nameObj, nullptr);
- name.setTo(nameStr);
- env->ReleaseStringUTFChars(nameObj, nameStr);
-
- CursorWindow* window;
- status_t status = CursorWindow::create(name, cursorWindowSize, &window);
- if (status || !window) {
- jniThrowExceptionFmt(
- env, "android/database/CursorWindowAllocationException",
- "Could not allocate CursorWindow '%s' of size %d due to error %d.",
- name.string(), cursorWindowSize, status);
- return 0;
- }
-
- LOG_WINDOW("nativeInitializeEmpty: window = %p", window);
- return reinterpret_cast<jlong>(window);
-}
-
-// static jlong nativeCreateFromParcel(JNIEnv* env, jclass clazz, jobject
-// parcelObj) {
-// Parcel* parcel = parcelForJavaObject(env, parcelObj);
-//
-// CursorWindow* window;
-// status_t status = CursorWindow::createFromParcel(parcel, &window);
-// if (status || !window) {
-// jniThrowExceptionFmt(env,
-// "android/database/CursorWindowAllocationException",
-// "Could not create CursorWindow from Parcel due to
-// error %d, process fd count=%d", status,
-// getFdCount());
-// return 0;
-// }
-//
-// LOG_WINDOW("nativeInitializeFromBinder: numRows = %d, numColumns = %d,
-// window = %p",
-// window->getNumRows(), window->getNumColumns(), window);
-// return reinterpret_cast<jlong>(window);
-// }
-
-static void nativeDispose(JNIEnv* env, jclass clazz, jlong windowPtr) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- if (window) {
- LOG_WINDOW("Closing window %p", window);
- delete window;
- }
-}
-
-static jstring nativeGetName(JNIEnv* env, jclass clazz, jlong windowPtr) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- return env->NewStringUTF(window->name().string());
-}
-//
-// static void nativeWriteToParcel(JNIEnv * env, jclass clazz, jlong windowPtr,
-// jobject parcelObj) {
-// CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
-// Parcel* parcel = parcelForJavaObject(env, parcelObj);
-//
-// status_t status = window->writeToParcel(parcel);
-// if (status) {
-// String8 msg;
-// msg.appendFormat("Could not write CursorWindow to Parcel due to error
-// %d.", status); jniThrowRuntimeException(env, msg.string());
-// }
-//}
-
-static void nativeClear(JNIEnv* env, jclass clazz, jlong windowPtr) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- LOG_WINDOW("Clearing window %p", window);
- status_t status = window->clear();
- if (status) {
- LOG_WINDOW("Could not clear window. error=%d", status);
- }
-}
-
-static jint nativeGetNumRows(JNIEnv* env, jclass clazz, jlong windowPtr) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- return window->getNumRows();
-}
-
-static jboolean nativeSetNumColumns(JNIEnv* env, jclass clazz, jlong windowPtr,
- jint columnNum) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- status_t status = window->setNumColumns(columnNum);
- return status == OK;
-}
-
-static jboolean nativeAllocRow(JNIEnv* env, jclass clazz, jlong windowPtr) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- status_t status = window->allocRow();
- return status == OK;
-}
-
-static void nativeFreeLastRow(JNIEnv* env, jclass clazz, jlong windowPtr) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- window->freeLastRow();
-}
-
-static jint nativeGetType(JNIEnv* env, jclass clazz, jlong windowPtr, jint row,
- jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- LOG_WINDOW("returning column type affinity for %d,%d from %p", row, column,
- window);
-
- CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
- if (!fieldSlot) {
- // FIXME: This is really broken but we have CTS tests that depend
- // on this legacy behavior.
- // throwExceptionWithRowCol(env, row, column);
- return CursorWindow::FIELD_TYPE_NULL;
- }
- return window->getFieldSlotType(fieldSlot);
-}
-
-static jbyteArray nativeGetBlob(JNIEnv* env, jclass clazz, jlong windowPtr,
- jint row, jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- LOG_WINDOW("Getting blob for %d,%d from %p", row, column, window);
-
- CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
- if (!fieldSlot) {
- throwExceptionWithRowCol(env, row, column);
- return nullptr;
- }
-
- int32_t type = window->getFieldSlotType(fieldSlot);
- if (type == CursorWindow::FIELD_TYPE_BLOB ||
- type == CursorWindow::FIELD_TYPE_STRING) {
- size_t size;
- const void* value = window->getFieldSlotValueBlob(fieldSlot, &size);
- if (!value) {
- throw_sqlite3_exception(env, "Native could not read blob slot");
- return nullptr;
- }
- jbyteArray byteArray = env->NewByteArray(size);
- if (!byteArray) {
- env->ExceptionClear();
- throw_sqlite3_exception(env, "Native could not create new byte[]");
- return nullptr;
- }
- env->SetByteArrayRegion(byteArray, 0, size,
- static_cast<const jbyte*>(value));
- return byteArray;
- } else if (type == CursorWindow::FIELD_TYPE_INTEGER) {
- throw_sqlite3_exception(env, "INTEGER data in nativeGetBlob ");
- } else if (type == CursorWindow::FIELD_TYPE_FLOAT) {
- throw_sqlite3_exception(env, "FLOAT data in nativeGetBlob ");
- } else if (type == CursorWindow::FIELD_TYPE_NULL) {
- // do nothing
- } else {
- throwUnknownTypeException(env, type);
- }
- return nullptr;
-}
-
-static jstring nativeGetString(JNIEnv* env, jclass clazz, jlong windowPtr,
- jint row, jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- LOG_WINDOW("Getting string for %d,%d from %p", row, column, window);
-
- CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
- if (!fieldSlot) {
- throwExceptionWithRowCol(env, row, column);
- return nullptr;
- }
-
- int32_t type = window->getFieldSlotType(fieldSlot);
- if (type == CursorWindow::FIELD_TYPE_STRING) {
- size_t sizeIncludingNull;
- const char* value =
- window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull);
- if (!value) {
- throw_sqlite3_exception(env, "Native could not read string slot");
- return nullptr;
- }
- if (sizeIncludingNull <= 1) {
- return gEmptyString;
- }
- // Convert to UTF-16 here instead of calling NewStringUTF. NewStringUTF
- // doesn't like UTF-8 strings with high codepoints. It actually expects
- // Modified UTF-8 with encoded surrogate pairs.
- String16 utf16(value, sizeIncludingNull - 1);
- return env->NewString(reinterpret_cast<const jchar*>(utf16.string()),
- utf16.size());
- } else if (type == CursorWindow::FIELD_TYPE_INTEGER) {
- int64_t value = window->getFieldSlotValueLong(fieldSlot);
- char buf[32];
- snprintf(buf, sizeof(buf), "%" PRId64, value);
- return env->NewStringUTF(buf);
- } else if (type == CursorWindow::FIELD_TYPE_FLOAT) {
- double value = window->getFieldSlotValueDouble(fieldSlot);
- char buf[32];
- snprintf(buf, sizeof(buf), "%g", value);
- return env->NewStringUTF(buf);
- } else if (type == CursorWindow::FIELD_TYPE_NULL) {
- return nullptr;
- } else if (type == CursorWindow::FIELD_TYPE_BLOB) {
- throw_sqlite3_exception(env, "Unable to convert BLOB to string");
- return nullptr;
- } else {
- throwUnknownTypeException(env, type);
- return nullptr;
- }
-}
-
-static jcharArray allocCharArrayBuffer(JNIEnv* env, jobject bufferObj,
- size_t size) {
- jcharArray dataObj = jcharArray(env->GetObjectField(
- bufferObj, getCharArrayBufferDataFieldId(env, bufferObj)));
- if (dataObj && size) {
- jsize capacity = env->GetArrayLength(dataObj);
- if (size_t(capacity) < size) {
- env->DeleteLocalRef(dataObj);
- dataObj = nullptr;
- }
- }
- if (!dataObj) {
- jsize capacity = size;
- if (capacity < 64) {
- capacity = 64;
- }
- dataObj = env->NewCharArray(capacity); // might throw OOM
- if (dataObj) {
- env->SetObjectField(
- bufferObj, getCharArrayBufferDataFieldId(env, bufferObj), dataObj);
- }
- }
- return dataObj;
-}
-
-static void fillCharArrayBufferUTF(JNIEnv* env, jobject bufferObj,
- const char* str, size_t len) {
- ssize_t size =
- utf8_to_utf16_length(reinterpret_cast<const uint8_t*>(str), len);
- if (size < 0) {
- size = 0; // invalid UTF8 string
- }
- jcharArray dataObj = allocCharArrayBuffer(env, bufferObj, size);
- if (dataObj) {
- if (size) {
- jchar* data =
- static_cast<jchar*>(env->GetPrimitiveArrayCritical(dataObj, nullptr));
- utf8_to_utf16_no_null_terminator(reinterpret_cast<const uint8_t*>(str),
- len, reinterpret_cast<char16_t*>(data),
- static_cast<size_t>(size));
- env->ReleasePrimitiveArrayCritical(dataObj, data, 0);
- }
- env->SetIntField(bufferObj,
- getCharArrayBufferSizeCopiedFieldId(env, bufferObj), size);
- }
-}
-
-static void clearCharArrayBuffer(JNIEnv* env, jobject bufferObj) {
- jcharArray dataObj = allocCharArrayBuffer(env, bufferObj, 0);
- if (dataObj) {
- env->SetIntField(bufferObj,
- getCharArrayBufferSizeCopiedFieldId(env, bufferObj), 0);
- }
-}
-
-static void nativeCopyStringToBuffer(JNIEnv* env, jclass clazz, jlong windowPtr,
- jint row, jint column, jobject bufferObj) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- LOG_WINDOW("Copying string for %d,%d from %p", row, column, window);
-
- CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
- if (!fieldSlot) {
- throwExceptionWithRowCol(env, row, column);
- return;
- }
-
- int32_t type = window->getFieldSlotType(fieldSlot);
- if (type == CursorWindow::FIELD_TYPE_STRING) {
- size_t sizeIncludingNull;
- const char* value =
- window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull);
- if (sizeIncludingNull > 1) {
- fillCharArrayBufferUTF(env, bufferObj, value, sizeIncludingNull - 1);
- } else {
- clearCharArrayBuffer(env, bufferObj);
- }
- } else if (type == CursorWindow::FIELD_TYPE_INTEGER) {
- int64_t value = window->getFieldSlotValueLong(fieldSlot);
- char buf[32];
- snprintf(buf, sizeof(buf), "%" PRId64, value);
- fillCharArrayBufferUTF(env, bufferObj, buf, strlen(buf));
- } else if (type == CursorWindow::FIELD_TYPE_FLOAT) {
- double value = window->getFieldSlotValueDouble(fieldSlot);
- char buf[32];
- snprintf(buf, sizeof(buf), "%g", value);
- fillCharArrayBufferUTF(env, bufferObj, buf, strlen(buf));
- } else if (type == CursorWindow::FIELD_TYPE_NULL) {
- clearCharArrayBuffer(env, bufferObj);
- } else if (type == CursorWindow::FIELD_TYPE_BLOB) {
- throw_sqlite3_exception(env, "Unable to convert BLOB to string");
- } else {
- throwUnknownTypeException(env, type);
- }
-}
-
-static jlong nativeGetLong(JNIEnv* env, jclass clazz, jlong windowPtr, jint row,
- jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- LOG_WINDOW("Getting long for %d,%d from %p", row, column, window);
-
- CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
- if (!fieldSlot) {
- throwExceptionWithRowCol(env, row, column);
- return 0;
- }
-
- int32_t type = window->getFieldSlotType(fieldSlot);
- if (type == CursorWindow::FIELD_TYPE_INTEGER) {
- return window->getFieldSlotValueLong(fieldSlot);
- } else if (type == CursorWindow::FIELD_TYPE_STRING) {
- size_t sizeIncludingNull;
- const char* value =
- window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull);
- return sizeIncludingNull > 1 ? strtoll(value, nullptr, 0) : 0L;
- } else if (type == CursorWindow::FIELD_TYPE_FLOAT) {
- return jlong(window->getFieldSlotValueDouble(fieldSlot));
- } else if (type == CursorWindow::FIELD_TYPE_NULL) {
- return 0;
- } else if (type == CursorWindow::FIELD_TYPE_BLOB) {
- throw_sqlite3_exception(env, "Unable to convert BLOB to long");
- return 0;
- } else {
- throwUnknownTypeException(env, type);
- return 0;
- }
-}
-
-static jdouble nativeGetDouble(JNIEnv* env, jclass clazz, jlong windowPtr,
- jint row, jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- LOG_WINDOW("Getting double for %d,%d from %p", row, column, window);
-
- CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
- if (!fieldSlot) {
- throwExceptionWithRowCol(env, row, column);
- return 0.0;
- }
-
- int32_t type = window->getFieldSlotType(fieldSlot);
- if (type == CursorWindow::FIELD_TYPE_FLOAT) {
- return window->getFieldSlotValueDouble(fieldSlot);
- } else if (type == CursorWindow::FIELD_TYPE_STRING) {
- size_t sizeIncludingNull;
- const char* value =
- window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull);
- return sizeIncludingNull > 1 ? strtod(value, nullptr) : 0.0;
- } else if (type == CursorWindow::FIELD_TYPE_INTEGER) {
- return jdouble(window->getFieldSlotValueLong(fieldSlot));
- } else if (type == CursorWindow::FIELD_TYPE_NULL) {
- return 0.0;
- } else if (type == CursorWindow::FIELD_TYPE_BLOB) {
- throw_sqlite3_exception(env, "Unable to convert BLOB to double");
- return 0.0;
- } else {
- throwUnknownTypeException(env, type);
- return 0.0;
- }
-}
-
-static jboolean nativePutBlob(JNIEnv* env, jclass clazz, jlong windowPtr,
- jbyteArray valueObj, jint row, jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- jsize len = env->GetArrayLength(valueObj);
-
- void* value = env->GetPrimitiveArrayCritical(valueObj, nullptr);
- status_t status = window->putBlob(row, column, value, len);
- env->ReleasePrimitiveArrayCritical(valueObj, value, JNI_ABORT);
-
- if (status) {
- LOG_WINDOW("Failed to put blob. error=%d", status);
- return false;
- }
-
- LOG_WINDOW("%d,%d is BLOB with %u bytes", row, column, len);
- return true;
-}
-
-static jboolean nativePutString(JNIEnv* env, jclass clazz, jlong windowPtr,
- jstring valueObj, jint row, jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
-
- size_t sizeIncludingNull = env->GetStringUTFLength(valueObj) + 1;
- const char* valueStr = env->GetStringUTFChars(valueObj, nullptr);
- if (!valueStr) {
- LOG_WINDOW("value can't be transferred to UTFChars");
- return false;
- }
- status_t status = window->putString(row, column, valueStr, sizeIncludingNull);
- env->ReleaseStringUTFChars(valueObj, valueStr);
-
- if (status) {
- LOG_WINDOW("Failed to put string. error=%d", status);
- return false;
- }
-
- LOG_WINDOW("%d,%d is TEXT with %zu bytes", row, column, sizeIncludingNull);
- return true;
-}
-
-static jboolean nativePutLong(JNIEnv* env, jclass clazz, jlong windowPtr,
- jlong value, jint row, jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- status_t status = window->putLong(row, column, value);
-
- if (status) {
- LOG_WINDOW("Failed to put long. error=%d", status);
- return false;
- }
-
- LOG_WINDOW("%d,%d is INTEGER %" PRId64, row, column, value);
- return true;
-}
-
-static jboolean nativePutDouble(JNIEnv* env, jclass clazz, jlong windowPtr,
- jdouble value, jint row, jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- status_t status = window->putDouble(row, column, value);
-
- if (status) {
- LOG_WINDOW("Failed to put double. error=%d", status);
- return false;
- }
-
- LOG_WINDOW("%d,%d is FLOAT %lf", row, column, value);
- return true;
-}
-
-static jboolean nativePutNull(JNIEnv* env, jclass clazz, jlong windowPtr,
- jint row, jint column) {
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
- status_t status = window->putNull(row, column);
-
- if (status) {
- LOG_WINDOW("Failed to put null. error=%d", status);
- return false;
- }
-
- LOG_WINDOW("%d,%d is NULL", row, column);
- return true;
-}
-
-static const JNINativeMethod sMethods[] = {
- /* name, signature, funcPtr */
- {(char*)"nativeCreate", (char*)"(Ljava/lang/String;I)J",
- reinterpret_cast<void*>(nativeCreate)},
- // { "nativeCreateFromParcel", "(Landroid/os/Parcel;)J",
- // (void*)nativeCreateFromParcel },
- {(char*)"nativeDispose", (char*)"(J)V",
- reinterpret_cast<void*>(nativeDispose)},
- // { "nativeWriteToParcel", "(JLandroid/os/Parcel;)V",
- // (void*)nativeWriteToParcel },
-
- {const_cast<char*>("nativeGetName"),
- const_cast<char*>("(J)Ljava/lang/String;"),
- reinterpret_cast<void*>(nativeGetName)},
- {const_cast<char*>("nativeGetBlob"), const_cast<char*>("(JII)[B"),
- reinterpret_cast<void*>(nativeGetBlob)},
- {const_cast<char*>("nativeGetString"),
- const_cast<char*>("(JII)Ljava/lang/String;"),
- reinterpret_cast<void*>(nativeGetString)},
- {const_cast<char*>("nativeCopyStringToBuffer"),
- const_cast<char*>("(JIILandroid/database/CharArrayBuffer;)V"),
- reinterpret_cast<void*>(nativeCopyStringToBuffer)},
- {const_cast<char*>("nativePutBlob"), const_cast<char*>("(J[BII)Z"),
- reinterpret_cast<void*>(nativePutBlob)},
- {const_cast<char*>("nativePutString"),
- const_cast<char*>("(JLjava/lang/String;II)Z"),
- reinterpret_cast<void*>(nativePutString)},
-
- // ------- @FastNative below here ----------------------
- {const_cast<char*>("nativeClear"), const_cast<char*>("(J)V"),
- reinterpret_cast<void*>(nativeClear)},
- {const_cast<char*>("nativeGetNumRows"), const_cast<char*>("(J)I"),
- reinterpret_cast<void*>(nativeGetNumRows)},
- {const_cast<char*>("nativeSetNumColumns"), const_cast<char*>("(JI)Z"),
- reinterpret_cast<void*>(nativeSetNumColumns)},
- {const_cast<char*>("nativeAllocRow"), const_cast<char*>("(J)Z"),
- reinterpret_cast<void*>(nativeAllocRow)},
- {const_cast<char*>("nativeFreeLastRow"), const_cast<char*>("(J)V"),
- reinterpret_cast<void*>(nativeFreeLastRow)},
- {const_cast<char*>("nativeGetType"), const_cast<char*>("(JII)I"),
- reinterpret_cast<void*>(nativeGetType)},
- {const_cast<char*>("nativeGetLong"), const_cast<char*>("(JII)J"),
- reinterpret_cast<void*>(nativeGetLong)},
- {const_cast<char*>("nativeGetDouble"), const_cast<char*>("(JII)D"),
- reinterpret_cast<void*>(nativeGetDouble)},
- {const_cast<char*>("nativePutLong"), const_cast<char*>("(JJII)Z"),
- reinterpret_cast<void*>(nativePutLong)},
- {const_cast<char*>("nativePutDouble"), const_cast<char*>("(JDII)Z"),
- reinterpret_cast<void*>(nativePutDouble)},
- {const_cast<char*>("nativePutNull"), const_cast<char*>("(JII)Z"),
- reinterpret_cast<void*>(nativePutNull)},
-};
-
-int register_android_database_CursorWindow(JNIEnv* env) {
- gEmptyString = (jstring)env->NewGlobalRef(env->NewStringUTF(""));
-
- static const char* kCursorWindowClass =
- "org/robolectric/nativeruntime/CursorWindowNatives";
-
- ScopedLocalRef<jclass> cls(env, env->FindClass(kCursorWindowClass));
-
- if (cls.get() == nullptr) {
- ALOGE("jni CursorWindow registration failure, class not found '%s'",
- kCursorWindowClass);
- return JNI_ERR;
- }
-
- const jint count = sizeof(sMethods) / sizeof(sMethods[0]);
- int status = env->RegisterNatives(cls.get(), sMethods, count);
- if (status < 0) {
- ALOGE("jni CursorWindow registration failure, status: %d", status);
- return JNI_ERR;
- }
- return JNI_VERSION_1_4;
-}
-
-} // namespace android
diff --git a/nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.cpp b/nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.cpp
deleted file mode 100644
index 0da34da0b..000000000
--- a/nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.cpp
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/core/jni/android_database_SQLiteCommon.cpp
-
-#include "robo_android_database_SQLiteCommon.h"
-
-#include <utils/String8.h>
-
-#include <map>
-#include <string>
-
-namespace android {
-
-static const std::map<int, std::string> sErrorCodesMap = {
- // Primary Result Code List
- {4, "SQLITE_ABORT"},
- {23, "SQLITE_AUTH"},
- {5, "SQLITE_BUSY"},
- {14, "SQLITE_CANTOPEN"},
- {19, "SQLITE_CONSTRAINT"},
- {11, "SQLITE_CORRUPT"},
- {101, "SQLITE_DONE"},
- {16, "SQLITE_EMPTY"},
- {1, "SQLITE_ERROR"},
- {24, "SQLITE_FORMAT"},
- {13, "SQLITE_FULL"},
- {2, "SQLITE_INTERNAL"},
- {9, "SQLITE_INTERRUPT"},
- {10, "SQLITE_IOERR"},
- {6, "SQLITE_LOCKED"},
- {20, "SQLITE_MISMATCH"},
- {21, "SQLITE_MISUSE"},
- {22, "SQLITE_NOLFS"},
- {7, "SQLITE_NOMEM"},
- {26, "SQLITE_NOTADB"},
- {12, "SQLITE_NOTFOUND"},
- {27, "SQLITE_NOTICE"},
- {0, "SQLITE_OK"},
- {3, "SQLITE_PERM"},
- {15, "SQLITE_PROTOCOL"},
- {25, "SQLITE_RANGE"},
- {8, "SQLITE_READONLY"},
- {100, "SQLITE_ROW"},
- {17, "SQLITE_SCHEMA"},
- {18, "SQLITE_TOOBIG"},
- {28, "SQLITE_WARNING"},
- // Extended Result Code List
- {516, "SQLITE_ABORT_ROLLBACK"},
- {261, "SQLITE_BUSY_RECOVERY"},
- {517, "SQLITE_BUSY_SNAPSHOT"},
- {1038, "SQLITE_CANTOPEN_CONVPATH"},
- {782, "SQLITE_CANTOPEN_FULLPATH"},
- {526, "SQLITE_CANTOPEN_ISDIR"},
- {270, "SQLITE_CANTOPEN_NOTEMPDIR"},
- {275, "SQLITE_CONSTRAINT_CHECK"},
- {531, "SQLITE_CONSTRAINT_COMMITHOOK"},
- {787, "SQLITE_CONSTRAINT_FOREIGNKEY"},
- {1043, "SQLITE_CONSTRAINT_FUNCTION"},
- {1299, "SQLITE_CONSTRAINT_NOTNULL"},
- {1555, "SQLITE_CONSTRAINT_PRIMARYKEY"},
- {2579, "SQLITE_CONSTRAINT_ROWID"},
- {1811, "SQLITE_CONSTRAINT_TRIGGER"},
- {2067, "SQLITE_CONSTRAINT_UNIQUE"},
- {2323, "SQLITE_CONSTRAINT_VTAB"},
- {267, "SQLITE_CORRUPT_VTAB"},
- {3338, "SQLITE_IOERR_ACCESS"},
- {2826, "SQLITE_IOERR_BLOCKED"},
- {3594, "SQLITE_IOERR_CHECKRESERVEDLOCK"},
- {4106, "SQLITE_IOERR_CLOSE"},
- {6666, "SQLITE_IOERR_CONVPATH"},
- {2570, "SQLITE_IOERR_DELETE"},
- {5898, "SQLITE_IOERR_DELETE_NOENT"},
- {4362, "SQLITE_IOERR_DIR_CLOSE"},
- {1290, "SQLITE_IOERR_DIR_FSYNC"},
- {1802, "SQLITE_IOERR_FSTAT"},
- {1034, "SQLITE_IOERR_FSYNC"},
- {6410, "SQLITE_IOERR_GETTEMPPATH"},
- {3850, "SQLITE_IOERR_LOCK"},
- {6154, "SQLITE_IOERR_MMAP"},
- {3082, "SQLITE_IOERR_NOMEM"},
- {2314, "SQLITE_IOERR_RDLOCK"},
- {266, "SQLITE_IOERR_READ"},
- {5642, "SQLITE_IOERR_SEEK"},
- {5130, "SQLITE_IOERR_SHMLOCK"},
- {5386, "SQLITE_IOERR_SHMMAP"},
- {4618, "SQLITE_IOERR_SHMOPEN"},
- {4874, "SQLITE_IOERR_SHMSIZE"},
- {522, "SQLITE_IOERR_SHORT_READ"},
- {1546, "SQLITE_IOERR_TRUNCATE"},
- {2058, "SQLITE_IOERR_UNLOCK"},
- {778, "SQLITE_IOERR_WRITE"},
- {262, "SQLITE_LOCKED_SHAREDCACHE"},
- {539, "SQLITE_NOTICE_RECOVER_ROLLBACK"},
- {283, "SQLITE_NOTICE_RECOVER_WAL"},
- {256, "SQLITE_OK_LOAD_PERMANENTLY"},
- {520, "SQLITE_READONLY_CANTLOCK"},
- {1032, "SQLITE_READONLY_DBMOVED"},
- {264, "SQLITE_READONLY_RECOVERY"},
- {776, "SQLITE_READONLY_ROLLBACK"},
- {284, "SQLITE_WARNING_AUTOINDEX"},
-};
-
-static std::string sqlite3_error_code_to_msg(int errcode) {
- auto it = sErrorCodesMap.find(errcode);
- if (it != sErrorCodesMap.end()) {
- return std::to_string(errcode) + " " + it->second;
- } else {
- return std::to_string(errcode);
- }
-}
-
-/* throw a SQLiteException with a message appropriate for the error in handle */
-void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle) {
- throw_sqlite3_exception(env, handle, nullptr);
-}
-
-/* throw a SQLiteException with the given message */
-void throw_sqlite3_exception(JNIEnv* env, const char* message) {
- throw_sqlite3_exception(env, nullptr, message);
-}
-
-/* throw a SQLiteException with a message appropriate for the error in handle
- concatenated with the given message
- */
-void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle,
- const char* message) {
- if (handle) {
- // get the error code and message from the SQLite connection
- // the error message may contain more information than the error code
- // because it is based on the extended error code rather than the simplified
- // error code that SQLite normally returns.
- throw_sqlite3_exception(env, sqlite3_extended_errcode(handle),
- sqlite3_errmsg(handle), message);
- } else {
- // we use SQLITE_OK so that a generic SQLiteException is thrown;
- // any code not specified in the switch statement below would do.
- throw_sqlite3_exception(env, SQLITE_OK, "unknown error", message);
- }
-}
-
-/* throw a SQLiteException for a given error code
- * should only be used when the database connection is not available because the
- * error information will not be quite as rich */
-void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode,
- const char* message) {
- throw_sqlite3_exception(env, errcode, "unknown error", message);
-}
-
-/* throw a SQLiteException for a given error code, sqlite3message, and
- user message
- */
-void throw_sqlite3_exception(JNIEnv* env, int errcode,
- const char* sqlite3Message, const char* message) {
- const char* exceptionClass;
- switch (errcode & 0xff) { /* mask off extended error code */
- case SQLITE_IOERR:
- exceptionClass = "android/database/sqlite/SQLiteDiskIOException";
- break;
- case SQLITE_CORRUPT:
- case SQLITE_NOTADB: // treat "unsupported file format" error as corruption
- // also
- exceptionClass = "android/database/sqlite/SQLiteDatabaseCorruptException";
- break;
- case SQLITE_CONSTRAINT:
- exceptionClass = "android/database/sqlite/SQLiteConstraintException";
- break;
- case SQLITE_ABORT:
- exceptionClass = "android/database/sqlite/SQLiteAbortException";
- break;
- case SQLITE_DONE:
- exceptionClass = "android/database/sqlite/SQLiteDoneException";
- sqlite3Message =
- nullptr; // SQLite error message is irrelevant in this case
- break;
- case SQLITE_FULL:
- exceptionClass = "android/database/sqlite/SQLiteFullException";
- break;
- case SQLITE_MISUSE:
- exceptionClass = "android/database/sqlite/SQLiteMisuseException";
- break;
- case SQLITE_PERM:
- exceptionClass = "android/database/sqlite/SQLiteAccessPermException";
- break;
- case SQLITE_BUSY:
- exceptionClass = "android/database/sqlite/SQLiteDatabaseLockedException";
- break;
- case SQLITE_LOCKED:
- exceptionClass = "android/database/sqlite/SQLiteTableLockedException";
- break;
- case SQLITE_READONLY:
- exceptionClass =
- "android/database/sqlite/SQLiteReadOnlyDatabaseException";
- break;
- case SQLITE_CANTOPEN:
- exceptionClass =
- "android/database/sqlite/SQLiteCantOpenDatabaseException";
- break;
- case SQLITE_TOOBIG:
- exceptionClass = "android/database/sqlite/SQLiteBlobTooBigException";
- break;
- case SQLITE_RANGE:
- exceptionClass =
- "android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException";
- break;
- case SQLITE_NOMEM:
- exceptionClass = "android/database/sqlite/SQLiteOutOfMemoryException";
- break;
- case SQLITE_MISMATCH:
- exceptionClass =
- "android/database/sqlite/SQLiteDatatypeMismatchException";
- break;
- case SQLITE_INTERRUPT:
- exceptionClass = "android/os/OperationCanceledException";
- break;
- default:
- exceptionClass = "android/database/sqlite/SQLiteException";
- break;
- }
-
- if (sqlite3Message) {
- String8 fullMessage;
- fullMessage.append(sqlite3Message);
- std::string errcode_msg = sqlite3_error_code_to_msg(errcode);
- fullMessage.appendFormat(" (code %s)",
- errcode_msg.c_str()); // print extended error code
- if (message) {
- fullMessage.append(": ");
- fullMessage.append(message);
- }
- jniThrowException(env, exceptionClass, fullMessage.string());
- } else {
- jniThrowException(env, exceptionClass, message);
- }
-}
-
-} // namespace android
diff --git a/nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.h b/nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.h
deleted file mode 100644
index a426ce271..000000000
--- a/nativeruntime/cpp/jni/robo_android_database_SQLiteCommon.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/core/jni/android_database_SQLiteCommon.h
-
-#ifndef _ANDROID_DATABASE_SQLITE_COMMON_H
-#define _ANDROID_DATABASE_SQLITE_COMMON_H
-
-#include <jni.h>
-#include <nativehelper/JNIHelp.h>
-#include <sqlite3.h>
-
-// Special log tags defined in SQLiteDebug.java.
-#define SQLITE_LOG_TAG "SQLiteLog"
-#define SQLITE_TRACE_TAG "SQLiteStatements"
-#define SQLITE_PROFILE_TAG "SQLiteTime"
-
-namespace android {
-
-/* throw a SQLiteException with a message appropriate for the error in handle */
-void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle);
-
-/* throw a SQLiteException with the given message */
-void throw_sqlite3_exception(JNIEnv* env, const char* message);
-
-/* throw a SQLiteException with a message appropriate for the error in handle
- concatenated with the given message
- */
-void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle, const char* message);
-
-/* throw a SQLiteException for a given error code */
-void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode,
- const char* message);
-
-void throw_sqlite3_exception(JNIEnv* env, int errcode,
- const char* sqlite3Message, const char* message);
-
-} // namespace android
-
-#endif // _ANDROID_DATABASE_SQLITE_COMMON_H
diff --git a/nativeruntime/cpp/jni/robo_android_database_SQLiteConnection.cpp b/nativeruntime/cpp/jni/robo_android_database_SQLiteConnection.cpp
deleted file mode 100644
index d12df8599..000000000
--- a/nativeruntime/cpp/jni/robo_android_database_SQLiteConnection.cpp
+++ /dev/null
@@ -1,1068 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/core/jni/android_database_SQLiteConnection.cpp
-
-#define LOG_TAG "SQLiteConnection"
-
-#include <jni.h>
-#include <nativehelper/JNIHelp.h>
-
-#include "AndroidRuntime.h"
-// #include <android_runtime/Log.h>
-
-#include <androidfw/CursorWindow.h>
-#include <cutils/ashmem.h>
-#include <log/log.h>
-#include <nativehelper/scoped_local_ref.h>
-#include <nativehelper/scoped_utf8_chars.h>
-#include <sqlite3.h>
-#include <sqlite3_android.h>
-#include <string.h>
-#if !defined(_WIN32)
-#include <sys/mman.h>
-#endif
-#include <unistd.h>
-#include <utils/String16.h>
-#include <utils/String8.h>
-
-#include "robo_android_database_SQLiteCommon.h"
-
-// #include "core_jni_helpers.h"
-
-// Set to 1 to use UTF16 storage for localized indexes.
-#define UTF16_STORAGE 0
-
-namespace android {
-
-/* Busy timeout in milliseconds.
- * If another connection (possibly in another process) has the database locked
- * for longer than this amount of time then SQLite will generate a SQLITE_BUSY
- * error. The SQLITE_BUSY error is then raised as a
- * SQLiteDatabaseLockedException.
- *
- * In ordinary usage, busy timeouts are quite rare. Most databases only ever
- * have a single open connection at a time unless they are using WAL. When
- * using WAL, a timeout could occur if one connection is busy performing an
- * auto-checkpoint operation. The busy timeout needs to be long enough to
- * tolerate slow I/O write operations but not so long as to cause the
- * application to hang indefinitely if there is a problem acquiring a database
- * lock.
- */
-static const int BUSY_TIMEOUT_MS = 2500;
-
-static struct { jmethodID apply; } gUnaryOperator;
-
-static struct { jmethodID apply; } gBinaryOperator;
-
-struct SQLiteConnection {
- // Open flags.
- // Must be kept in sync with the constants defined in SQLiteDatabase.java.
- enum {
- OPEN_READWRITE = 0x00000000,
- OPEN_READONLY = 0x00000001,
- OPEN_READ_MASK = 0x00000001,
- NO_LOCALIZED_COLLATORS = 0x00000010,
- CREATE_IF_NECESSARY = 0x10000000,
- };
-
- sqlite3* const db;
- const int openFlags;
- const String8 path;
- const String8 label;
-
- volatile bool canceled;
-
- SQLiteConnection(sqlite3* db, int openFlags, const String8& path,
- const String8& label)
- : db(db),
- openFlags(openFlags),
- path(path),
- label(label),
- canceled(false) {}
-};
-
-// Called each time a statement begins execution, when tracing is enabled.
-static void sqliteTraceCallback(void* data, const char* sql) {
- SQLiteConnection* connection = static_cast<SQLiteConnection*>(data);
- ALOG(LOG_VERBOSE, SQLITE_TRACE_TAG, "%s: \"%s\"\n",
- connection->label.string(), sql);
-}
-
-// Called each time a statement finishes execution, when profiling is enabled.
-static void sqliteProfileCallback(void* data, const char* sql,
- sqlite3_uint64 tm) {
- SQLiteConnection* connection = static_cast<SQLiteConnection*>(data);
- ALOG(LOG_VERBOSE, SQLITE_PROFILE_TAG, "%s: \"%s\" took %0.3f ms\n",
- connection->label.string(), sql, tm * 0.000001f);
-}
-
-// Called after each SQLite VM instruction when cancelation is enabled.
-static int sqliteProgressHandlerCallback(void* data) {
- SQLiteConnection* connection = static_cast<SQLiteConnection*>(data);
- return connection->canceled;
-}
-
-static jlong nativeOpen(JNIEnv* env, jclass clazz, jstring pathStr,
- jint openFlags, jstring labelStr, jboolean enableTrace,
- jboolean enableProfile, jint lookasideSz,
- jint lookasideCnt) {
- int sqliteFlags;
- if (openFlags & SQLiteConnection::CREATE_IF_NECESSARY) {
- sqliteFlags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
- } else if (openFlags & SQLiteConnection::OPEN_READONLY) {
- sqliteFlags = SQLITE_OPEN_READONLY;
- } else {
- sqliteFlags = SQLITE_OPEN_READWRITE;
- }
-
- const char* pathChars = env->GetStringUTFChars(pathStr, nullptr);
- String8 path(pathChars);
- env->ReleaseStringUTFChars(pathStr, pathChars);
-
- const char* labelChars = env->GetStringUTFChars(labelStr, nullptr);
- String8 label(labelChars);
- env->ReleaseStringUTFChars(labelStr, labelChars);
-
- sqlite3* db;
- int err = sqlite3_open_v2(path.string(), &db, sqliteFlags, nullptr);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception_errcode(env, err, "Could not open database");
- return 0;
- }
-
- if (lookasideSz >= 0 && lookasideCnt >= 0) {
- int err = sqlite3_db_config(db, SQLITE_DBCONFIG_LOOKASIDE, NULL,
- lookasideSz, lookasideCnt);
- if (err != SQLITE_OK) {
- ALOGE("sqlite3_db_config(..., %d, %d) failed: %d", lookasideSz,
- lookasideCnt, err);
- throw_sqlite3_exception(env, db, "Cannot set lookaside");
- sqlite3_close(db);
- return 0;
- }
- }
-
- // Check that the database is really read/write when that is what we asked
- // for.
- if ((sqliteFlags & SQLITE_OPEN_READWRITE) &&
- sqlite3_db_readonly(db, nullptr)) {
- throw_sqlite3_exception(env, db,
- "Could not open the database in read/write mode.");
- sqlite3_close(db);
- return 0;
- }
-
- // Set the default busy handler to retry automatically before returning
- // SQLITE_BUSY.
- err = sqlite3_busy_timeout(db, BUSY_TIMEOUT_MS);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, db, "Could not set busy timeout");
- sqlite3_close(db);
- return 0;
- }
-
- // Register custom Android functions.
- err = register_android_functions(db, UTF16_STORAGE);
- if (err) {
- throw_sqlite3_exception(env, db,
- "Could not register Android SQL functions.");
- sqlite3_close(db);
- return 0;
- }
-
- // Create wrapper object.
- SQLiteConnection* connection =
- new SQLiteConnection(db, openFlags, path, label);
-
- // Enable tracing and profiling if requested.
- if (enableTrace) {
- sqlite3_trace(db, &sqliteTraceCallback, connection);
- }
- if (enableProfile) {
- sqlite3_profile(db, &sqliteProfileCallback, connection);
- }
-
- ALOGV("Opened connection %p with label '%s'", db, label.string());
- return reinterpret_cast<jlong>(connection);
-}
-
-static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
-
- if (connection) {
- ALOGV("Closing connection %p", connection->db);
- int err = sqlite3_close(connection->db);
- if (err != SQLITE_OK) {
- // This can happen if sub-objects aren't closed first. Make sure the
- // caller knows.
- ALOGE("sqlite3_close(%p) failed: %d", connection->db, err);
- throw_sqlite3_exception(env, connection->db, "Count not close db.");
- return;
- }
-
- delete connection;
- }
-}
-
-static void sqliteCustomScalarFunctionCallback(sqlite3_context* context,
- int argc, sqlite3_value** argv) {
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- jobject functionObjGlobal =
- reinterpret_cast<jobject>(sqlite3_user_data(context));
- ScopedLocalRef<jobject> functionObj(env, env->NewLocalRef(functionObjGlobal));
- ScopedLocalRef<jstring> argString(
- env, env->NewStringUTF(
- reinterpret_cast<const char*>(sqlite3_value_text(argv[0]))));
- ScopedLocalRef<jstring> resString(
- env, (jstring)env->CallObjectMethod(
- functionObj.get(), gUnaryOperator.apply, argString.get()));
-
- if (env->ExceptionCheck()) {
- ALOGE("Exception thrown by custom scalar function");
- sqlite3_result_error(context, "Exception thrown by custom scalar function",
- -1);
- env->ExceptionDescribe();
- env->ExceptionClear();
- return;
- }
-
- if (resString.get() == nullptr) {
- sqlite3_result_null(context);
- } else {
- ScopedUtfChars res(env, resString.get());
- sqlite3_result_text(context, res.c_str(), -1, SQLITE_TRANSIENT);
- }
-}
-
-static void sqliteCustomScalarFunctionDestructor(void* data) {
- jobject functionObjGlobal = reinterpret_cast<jobject>(data);
-
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- env->DeleteGlobalRef(functionObjGlobal);
-}
-
-static void nativeRegisterCustomScalarFunction(JNIEnv* env, jclass clazz,
- jlong connectionPtr,
- jstring functionName,
- jobject functionObj) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
-
- jobject functionObjGlobal = env->NewGlobalRef(functionObj);
- ScopedUtfChars functionNameChars(env, functionName);
- int err = sqlite3_create_function_v2(
- connection->db, functionNameChars.c_str(), 1, SQLITE_UTF8,
- reinterpret_cast<void*>(functionObjGlobal),
- &sqliteCustomScalarFunctionCallback, nullptr, nullptr,
- &sqliteCustomScalarFunctionDestructor);
-
- if (err != SQLITE_OK) {
- ALOGE("sqlite3_create_function returned %d", err);
- env->DeleteGlobalRef(functionObjGlobal);
- throw_sqlite3_exception(env, connection->db);
- return;
- }
-}
-
-static void sqliteCustomAggregateFunctionStep(sqlite3_context* context,
- int argc, sqlite3_value** argv) {
- char** agg = reinterpret_cast<char**>(
- sqlite3_aggregate_context(context, sizeof(const char**)));
- if (agg == nullptr) {
- return;
- } else if (*agg == nullptr) {
- // During our first call the best we can do is allocate our result
- // holder and populate it with our first value; we'll reduce it
- // against any additional values in future calls
- const char* res =
- reinterpret_cast<const char*>(sqlite3_value_text(argv[0]));
- if (res == nullptr) {
- *agg = nullptr;
- } else {
- *agg = strdup(res);
- }
- return;
- }
-
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- jobject functionObjGlobal =
- reinterpret_cast<jobject>(sqlite3_user_data(context));
- ScopedLocalRef<jobject> functionObj(env, env->NewLocalRef(functionObjGlobal));
- ScopedLocalRef<jstring> arg0String(
- env, env->NewStringUTF(reinterpret_cast<const char*>(*agg)));
- ScopedLocalRef<jstring> arg1String(
- env, env->NewStringUTF(
- reinterpret_cast<const char*>(sqlite3_value_text(argv[0]))));
- ScopedLocalRef<jstring> resString(
- env,
- (jstring)env->CallObjectMethod(functionObj.get(), gBinaryOperator.apply,
- arg0String.get(), arg1String.get()));
-
- if (env->ExceptionCheck()) {
- ALOGE("Exception thrown by custom aggregate function");
- sqlite3_result_error(context,
- "Exception thrown by custom aggregate function", -1);
- env->ExceptionDescribe();
- env->ExceptionClear();
- return;
- }
-
- // One way or another, we have a new value to collect, and we need to
- // free our previous value
- if (*agg != nullptr) {
- free(*agg);
- }
- if (resString.get() == nullptr) {
- *agg = nullptr;
- } else {
- ScopedUtfChars res(env, resString.get());
- *agg = strdup(res.c_str());
- }
-}
-
-static void sqliteCustomAggregateFunctionFinal(sqlite3_context* context) {
- // We pass zero size here to avoid allocating for empty sets
- char** agg = reinterpret_cast<char**>(sqlite3_aggregate_context(context, 0));
- if (agg == nullptr) {
- return;
- } else if (*agg == nullptr) {
- sqlite3_result_null(context);
- } else {
- sqlite3_result_text(context, *agg, -1, SQLITE_TRANSIENT);
- free(*agg);
- }
-}
-
-static void sqliteCustomAggregateFunctionDestructor(void* data) {
- jobject functionObjGlobal = reinterpret_cast<jobject>(data);
-
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- env->DeleteGlobalRef(functionObjGlobal);
-}
-
-static void nativeRegisterCustomAggregateFunction(JNIEnv* env, jclass clazz,
- jlong connectionPtr,
- jstring functionName,
- jobject functionObj) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
-
- jobject functionObjGlobal = env->NewGlobalRef(functionObj);
- ScopedUtfChars functionNameChars(env, functionName);
- int err = sqlite3_create_function_v2(
- connection->db, functionNameChars.c_str(), 1, SQLITE_UTF8,
- reinterpret_cast<void*>(functionObjGlobal), nullptr,
- &sqliteCustomAggregateFunctionStep, &sqliteCustomAggregateFunctionFinal,
- &sqliteCustomAggregateFunctionDestructor);
-
- if (err != SQLITE_OK) {
- ALOGE("sqlite3_create_function returned %d", err);
- env->DeleteGlobalRef(functionObjGlobal);
- throw_sqlite3_exception(env, connection->db);
- return;
- }
-}
-
-static void nativeRegisterLocalizedCollators(JNIEnv* env, jclass clazz,
- jlong connectionPtr,
- jstring localeStr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
-
- const char* locale = env->GetStringUTFChars(localeStr, nullptr);
- int err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
- env->ReleaseStringUTFChars(localeStr, locale);
-
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, connection->db);
- }
-}
-
-static jlong nativePrepareStatement(JNIEnv* env, jclass clazz,
- jlong connectionPtr, jstring sqlString) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
-
- jsize sqlLength = env->GetStringLength(sqlString);
- const jchar* sql = env->GetStringCritical(sqlString, nullptr);
- sqlite3_stmt* statement;
- int err = sqlite3_prepare16_v2(connection->db, sql, sqlLength * sizeof(jchar),
- &statement, nullptr);
- env->ReleaseStringCritical(sqlString, sql);
-
- if (err != SQLITE_OK) {
- // Error messages like 'near ")": syntax error' are not
- // always helpful enough, so construct an error string that
- // includes the query itself.
- const char* query = env->GetStringUTFChars(sqlString, nullptr);
- char* message = static_cast<char*>(malloc(strlen(query) + 50));
- if (message) {
- strcpy(message, ", while compiling: "); // less than 50 chars
- strcat(message, query);
- }
- env->ReleaseStringUTFChars(sqlString, query);
- throw_sqlite3_exception(env, connection->db, message);
- free(message);
- return 0;
- }
-
- ALOGV("Prepared statement %p on connection %p", statement, connection->db);
- return reinterpret_cast<jlong>(statement);
-}
-
-static void nativeFinalizeStatement(JNIEnv* env, jclass clazz,
- jlong connectionPtr, jlong statementPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- // We ignore the result of sqlite3_finalize because it is really telling us
- // about whether any errors occurred while executing the statement. The
- // statement itself is always finalized regardless.
- ALOGV("Finalized statement %p on connection %p", statement, connection->db);
- sqlite3_finalize(statement);
-}
-
-static jint nativeGetParameterCount(JNIEnv* env, jclass clazz,
- jlong connectionPtr, jlong statementPtr) {
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- return sqlite3_bind_parameter_count(statement);
-}
-
-static jboolean nativeIsReadOnly(JNIEnv* env, jclass clazz, jlong connectionPtr,
- jlong statementPtr) {
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- return sqlite3_stmt_readonly(statement) != 0;
-}
-
-static jint nativeGetColumnCount(JNIEnv* env, jclass clazz, jlong connectionPtr,
- jlong statementPtr) {
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- return sqlite3_column_count(statement);
-}
-
-static jstring nativeGetColumnName(JNIEnv* env, jclass clazz,
- jlong connectionPtr, jlong statementPtr,
- jint index) {
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- const jchar* name =
- static_cast<const jchar*>(sqlite3_column_name16(statement, index));
- if (name) {
- size_t length = 0;
- while (name[length]) {
- length += 1;
- }
- return env->NewString(name, length);
- }
- return nullptr;
-}
-
-static void nativeBindNull(JNIEnv* env, jclass clazz, jlong connectionPtr,
- jlong statementPtr, jint index) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = sqlite3_bind_null(statement, index);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, connection->db, nullptr);
- }
-}
-
-static void nativeBindLong(JNIEnv* env, jclass clazz, jlong connectionPtr,
- jlong statementPtr, jint index, jlong value) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = sqlite3_bind_int64(statement, index, value);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, connection->db, nullptr);
- }
-}
-
-static void nativeBindDouble(JNIEnv* env, jclass clazz, jlong connectionPtr,
- jlong statementPtr, jint index, jdouble value) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = sqlite3_bind_double(statement, index, value);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, connection->db, nullptr);
- }
-}
-
-static void nativeBindString(JNIEnv* env, jclass clazz, jlong connectionPtr,
- jlong statementPtr, jint index,
- jstring valueString) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- jsize valueLength = env->GetStringLength(valueString);
- const jchar* value = env->GetStringCritical(valueString, nullptr);
- int err = sqlite3_bind_text16(statement, index, value,
- valueLength * sizeof(jchar), SQLITE_TRANSIENT);
- env->ReleaseStringCritical(valueString, value);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, connection->db, nullptr);
- }
-}
-
-static void nativeBindBlob(JNIEnv* env, jclass clazz, jlong connectionPtr,
- jlong statementPtr, jint index,
- jbyteArray valueArray) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- jsize valueLength = env->GetArrayLength(valueArray);
- jbyte* value =
- static_cast<jbyte*>(env->GetPrimitiveArrayCritical(valueArray, nullptr));
- int err =
- sqlite3_bind_blob(statement, index, value, valueLength, SQLITE_TRANSIENT);
- env->ReleasePrimitiveArrayCritical(valueArray, value, JNI_ABORT);
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, connection->db, nullptr);
- }
-}
-
-static void nativeResetStatementAndClearBindings(JNIEnv* env, jclass clazz,
- jlong connectionPtr,
- jlong statementPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = sqlite3_reset(statement);
- if (err == SQLITE_OK) {
- err = sqlite3_clear_bindings(statement);
- }
- if (err != SQLITE_OK) {
- throw_sqlite3_exception(env, connection->db, nullptr);
- }
-}
-
-static int executeNonQuery(JNIEnv* env, SQLiteConnection* connection,
- sqlite3_stmt* statement, bool isPragmaStmt) {
- int rc = sqlite3_step(statement);
- if (isPragmaStmt) {
- while (rc == SQLITE_ROW) {
- rc = sqlite3_step(statement);
- }
- }
- if (rc == SQLITE_ROW) {
- throw_sqlite3_exception(env,
- "Queries can be performed using SQLiteDatabase "
- "query or rawQuery methods only.");
- } else if (rc != SQLITE_DONE) {
- throw_sqlite3_exception(env, connection->db);
- }
- return rc;
-}
-
-static void nativeExecute(JNIEnv* env, jclass clazz, jlong connectionPtr,
- jlong statementPtr, jboolean isPragmaStmt) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- executeNonQuery(env, connection, statement, isPragmaStmt);
-}
-
-static jint nativeExecuteForChangedRowCount(JNIEnv* env, jclass clazz,
- jlong connectionPtr,
- jlong statementPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = executeNonQuery(env, connection, statement, false);
- return err == SQLITE_DONE ? sqlite3_changes(connection->db) : -1;
-}
-
-static jlong nativeExecuteForLastInsertedRowId(JNIEnv* env, jclass clazz,
- jlong connectionPtr,
- jlong statementPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = executeNonQuery(env, connection, statement, false);
- return err == SQLITE_DONE && sqlite3_changes(connection->db) > 0
- ? sqlite3_last_insert_rowid(connection->db)
- : -1;
-}
-
-static int executeOneRowQuery(JNIEnv* env, SQLiteConnection* connection,
- sqlite3_stmt* statement) {
- int err = sqlite3_step(statement);
- if (err != SQLITE_ROW) {
- throw_sqlite3_exception(env, connection->db);
- }
- return err;
-}
-
-static jlong nativeExecuteForLong(JNIEnv* env, jclass clazz,
- jlong connectionPtr, jlong statementPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = executeOneRowQuery(env, connection, statement);
- if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
- return sqlite3_column_int64(statement, 0);
- }
- return -1;
-}
-
-static jstring nativeExecuteForString(JNIEnv* env, jclass clazz,
- jlong connectionPtr, jlong statementPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = executeOneRowQuery(env, connection, statement);
- if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
- const jchar* text =
- static_cast<const jchar*>(sqlite3_column_text16(statement, 0));
- if (text) {
- size_t length = sqlite3_column_bytes16(statement, 0) / sizeof(jchar);
- return env->NewString(text, length);
- }
- }
- return nullptr;
-}
-
-#if defined(_WIN32)
-static int createAshmemRegionWithData(JNIEnv* env, const void* data,
- size_t length) {
- jniThrowIOException(env, -1);
- return -1;
-}
-#else
-static int createAshmemRegionWithData(JNIEnv* env, const void* data,
- size_t length) {
- int error = 0;
- int fd = ashmem_create_region(nullptr, length);
- if (fd < 0) {
- error = errno;
- ALOGE("ashmem_create_region failed: %s", strerror(error));
- } else {
- if (length > 0) {
- void* ptr =
- mmap(nullptr, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- if (ptr == MAP_FAILED) {
- error = errno;
- ALOGE("mmap failed: %s", strerror(error));
- } else {
- memcpy(ptr, data, length);
- munmap(ptr, length);
- }
- }
-
- if (!error) {
- if (ashmem_set_prot_region(fd, PROT_READ) < 0) {
- error = errno;
- ALOGE("ashmem_set_prot_region failed: %s", strerror(errno));
- } else {
- return fd;
- }
- }
-
- close(fd);
- }
-
- jniThrowIOException(env, error);
- return -1;
-}
-#endif
-
-static jint nativeExecuteForBlobFileDescriptor(JNIEnv* env, jclass clazz,
- jlong connectionPtr,
- jlong statementPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
-
- int err = executeOneRowQuery(env, connection, statement);
- if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
- const void* blob = sqlite3_column_blob(statement, 0);
- if (blob) {
- int length = sqlite3_column_bytes(statement, 0);
- if (length >= 0) {
- return createAshmemRegionWithData(env, blob, length);
- }
- }
- }
- return -1;
-}
-
-enum CopyRowResult {
- CPR_OK,
- CPR_FULL,
- CPR_ERROR,
-};
-
-static CopyRowResult copyRow(JNIEnv* env, CursorWindow* window,
- sqlite3_stmt* statement, int numColumns,
- int startPos, int addedRows) {
- // Allocate a new field directory for the row.
- status_t status = window->allocRow();
- if (status) {
- LOG_WINDOW("Failed allocating fieldDir at startPos %d row %d, error=%d",
- startPos, addedRows, status);
- return CPR_FULL;
- }
-
- // Pack the row into the window.
- CopyRowResult result = CPR_OK;
- for (int i = 0; i < numColumns; i++) {
- int type = sqlite3_column_type(statement, i);
- if (type == SQLITE_TEXT) {
- // TEXT data
- const char* text =
- reinterpret_cast<const char*>(sqlite3_column_text(statement, i));
- // SQLite does not include the NULL terminator in size, but does
- // ensure all strings are NULL terminated, so increase size by
- // one to make sure we store the terminator.
- size_t sizeIncludingNull = sqlite3_column_bytes(statement, i) + 1;
- status = window->putString(addedRows, i, text, sizeIncludingNull);
- if (status) {
- LOG_WINDOW("Failed allocating %zu bytes for text at %d,%d, error=%d",
- sizeIncludingNull, startPos + addedRows, i, status);
- result = CPR_FULL;
- break;
- }
- LOG_WINDOW("%d,%d is TEXT with %zu bytes", startPos + addedRows, i,
- sizeIncludingNull);
- } else if (type == SQLITE_INTEGER) {
- // INTEGER data
- int64_t value = sqlite3_column_int64(statement, i);
- status = window->putLong(addedRows, i, value);
- if (status) {
- LOG_WINDOW("Failed allocating space for a long in column %d, error=%d",
- i, status);
- result = CPR_FULL;
- break;
- }
- LOG_WINDOW("%d,%d is INTEGER %" PRId64, startPos + addedRows, i, value);
- } else if (type == SQLITE_FLOAT) {
- // FLOAT data
- double value = sqlite3_column_double(statement, i);
- status = window->putDouble(addedRows, i, value);
- if (status) {
- LOG_WINDOW(
- "Failed allocating space for a double in column %d, error=%d", i,
- status);
- result = CPR_FULL;
- break;
- }
- LOG_WINDOW("%d,%d is FLOAT %lf", startPos + addedRows, i, value);
- } else if (type == SQLITE_BLOB) {
- // BLOB data
- const void* blob = sqlite3_column_blob(statement, i);
- size_t size = sqlite3_column_bytes(statement, i);
- status = window->putBlob(addedRows, i, blob, size);
- if (status) {
- LOG_WINDOW("Failed allocating %zu bytes for blob at %d,%d, error=%d",
- size, startPos + addedRows, i, status);
- result = CPR_FULL;
- break;
- }
- LOG_WINDOW("%d,%d is Blob with %zu bytes", startPos + addedRows, i, size);
- } else if (type == SQLITE_NULL) {
- // NULL field
- status = window->putNull(addedRows, i);
- if (status) {
- LOG_WINDOW("Failed allocating space for a null in column %d, error=%d",
- i, status);
- result = CPR_FULL;
- break;
- }
-
- LOG_WINDOW("%d,%d is NULL", startPos + addedRows, i);
- } else {
- // Unknown data
- ALOGE("Unknown column type when filling database window");
- throw_sqlite3_exception(env, "Unknown column type when filling window");
- result = CPR_ERROR;
- break;
- }
- }
-
- // Free the last row if it was not successfully copied.
- if (result != CPR_OK) {
- window->freeLastRow();
- }
- return result;
-}
-
-static jlong nativeExecuteForCursorWindow(JNIEnv* env, jclass clazz,
- jlong connectionPtr,
- jlong statementPtr, jlong windowPtr,
- jint startPos, jint requiredPos,
- jboolean countAllRows) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr);
- CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr);
-
- status_t status = window->clear();
- if (status) {
- String8 msg;
- msg.appendFormat("Failed to clear the cursor window, status=%d", status);
- throw_sqlite3_exception(env, connection->db, msg.string());
- return 0;
- }
-
- int numColumns = sqlite3_column_count(statement);
- status = window->setNumColumns(numColumns);
- if (status) {
- String8 msg;
- msg.appendFormat(
- "Failed to set the cursor window column count to %d, status=%d",
- numColumns, status);
- throw_sqlite3_exception(env, connection->db, msg.string());
- return 0;
- }
-
- int retryCount = 0;
- int totalRows = 0;
- int addedRows = 0;
- bool windowFull = false;
- bool gotException = false;
- while (!gotException && (!windowFull || countAllRows)) {
- int err = sqlite3_step(statement);
- if (err == SQLITE_ROW) {
- LOG_WINDOW("Stepped statement %p to row %d", statement, totalRows);
- retryCount = 0;
- totalRows += 1;
-
- // Skip the row if the window is full or we haven't reached the start
- // position yet.
- if (startPos >= totalRows || windowFull) {
- continue;
- }
-
- CopyRowResult cpr =
- copyRow(env, window, statement, numColumns, startPos, addedRows);
- if (cpr == CPR_FULL && addedRows && startPos + addedRows <= requiredPos) {
- // We filled the window before we got to the one row that we really
- // wanted. Clear the window and start filling it again from here.
- // TODO: Would be nicer if we could progressively replace earlier rows.
- window->clear();
- window->setNumColumns(numColumns);
- startPos += addedRows;
- addedRows = 0;
- cpr = copyRow(env, window, statement, numColumns, startPos, addedRows);
- }
-
- if (cpr == CPR_OK) {
- addedRows += 1;
- } else if (cpr == CPR_FULL) {
- windowFull = true;
- } else {
- gotException = true;
- }
- } else if (err == SQLITE_DONE) {
- // All rows processed, bail
- LOG_WINDOW("Processed all rows");
- break;
- } else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) {
- // The table is locked, retry
- LOG_WINDOW("Database locked, retrying");
- if (retryCount > 50) {
- ALOGE("Bailing on database busy retry");
- throw_sqlite3_exception(env, connection->db, "retrycount exceeded");
- gotException = true;
- } else {
- // Sleep to give the thread holding the lock a chance to finish
- usleep(1000);
- retryCount++;
- }
- } else {
- throw_sqlite3_exception(env, connection->db);
- gotException = true;
- }
- }
-
- LOG_WINDOW(
- "Resetting statement %p after fetching %d rows and adding %d rows "
- "to the window in %zu bytes",
- statement, totalRows, addedRows, window->size() - window->freeSpace());
- sqlite3_reset(statement);
-
- // Report the total number of rows on request.
- if (startPos > totalRows) {
- ALOGE("startPos %d > actual rows %d", startPos, totalRows);
- }
- if (totalRows > 0 && addedRows == 0) {
- String8 msg;
- msg.appendFormat(
- "Row too big to fit into CursorWindow requiredPos=%d, totalRows=%d",
- requiredPos, totalRows);
- throw_sqlite3_exception(env, SQLITE_TOOBIG, nullptr, msg.string());
- return 0;
- }
-
- jlong result = jlong(startPos) << 32 | jlong(totalRows);
- return result;
-}
-
-static jint nativeGetDbLookaside(JNIEnv* env, jobject clazz,
- jlong connectionPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
-
- int cur = -1;
- int unused;
- sqlite3_db_status(connection->db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur,
- &unused, 0);
- return cur;
-}
-
-static void nativeCancel(JNIEnv* env, jobject clazz, jlong connectionPtr) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- connection->canceled = true;
-}
-
-static void nativeResetCancel(JNIEnv* env, jobject clazz, jlong connectionPtr,
- jboolean cancelable) {
- SQLiteConnection* connection =
- reinterpret_cast<SQLiteConnection*>(connectionPtr);
- connection->canceled = false;
-
- if (cancelable) {
- sqlite3_progress_handler(connection->db, 4, sqliteProgressHandlerCallback,
- connection);
- } else {
- sqlite3_progress_handler(connection->db, 0, nullptr, nullptr);
- }
-}
-
-static const JNINativeMethod sMethods[] = {
- /* name, signature, funcPtr */
- {const_cast<char*>("nativeOpen"),
- const_cast<char*>("(Ljava/lang/String;ILjava/lang/String;ZZII)J"),
- reinterpret_cast<void*>(nativeOpen)},
- {const_cast<char*>("nativeClose"), const_cast<char*>("(J)V"),
- reinterpret_cast<void*>(nativeClose)},
- {const_cast<char*>("nativeRegisterCustomScalarFunction"),
- const_cast<char*>(
- "(JLjava/lang/String;Ljava/util/function/UnaryOperator;)V"),
- reinterpret_cast<void*>(nativeRegisterCustomScalarFunction)},
- {const_cast<char*>("nativeRegisterCustomAggregateFunction"),
- const_cast<char*>(
- "(JLjava/lang/String;Ljava/util/function/BinaryOperator;)V"),
- reinterpret_cast<void*>(nativeRegisterCustomAggregateFunction)},
- {const_cast<char*>("nativeRegisterLocalizedCollators"),
- const_cast<char*>("(JLjava/lang/String;)V"),
- reinterpret_cast<void*>(nativeRegisterLocalizedCollators)},
- {const_cast<char*>("nativePrepareStatement"),
- const_cast<char*>("(JLjava/lang/String;)J"),
- reinterpret_cast<void*>(nativePrepareStatement)},
- {const_cast<char*>("nativeFinalizeStatement"), const_cast<char*>("(JJ)V"),
- reinterpret_cast<void*>(nativeFinalizeStatement)},
- {const_cast<char*>("nativeGetParameterCount"), const_cast<char*>("(JJ)I"),
- reinterpret_cast<void*>(nativeGetParameterCount)},
- {const_cast<char*>("nativeIsReadOnly"), const_cast<char*>("(JJ)Z"),
- reinterpret_cast<void*>(nativeIsReadOnly)},
- {const_cast<char*>("nativeGetColumnCount"), const_cast<char*>("(JJ)I"),
- reinterpret_cast<void*>(nativeGetColumnCount)},
- {const_cast<char*>("nativeGetColumnName"),
- const_cast<char*>("(JJI)Ljava/lang/String;"),
- reinterpret_cast<void*>(nativeGetColumnName)},
- {const_cast<char*>("nativeBindNull"), const_cast<char*>("(JJI)V"),
- reinterpret_cast<void*>(nativeBindNull)},
- {const_cast<char*>("nativeBindLong"), const_cast<char*>("(JJIJ)V"),
- reinterpret_cast<void*>(nativeBindLong)},
- {const_cast<char*>("nativeBindDouble"), const_cast<char*>("(JJID)V"),
- reinterpret_cast<void*>(nativeBindDouble)},
- {const_cast<char*>("nativeBindString"),
- const_cast<char*>("(JJILjava/lang/String;)V"),
- reinterpret_cast<void*>(nativeBindString)},
- {const_cast<char*>("nativeBindBlob"), const_cast<char*>("(JJI[B)V"),
- reinterpret_cast<void*>(nativeBindBlob)},
- {const_cast<char*>("nativeResetStatementAndClearBindings"),
- const_cast<char*>("(JJ)V"),
- reinterpret_cast<void*>(nativeResetStatementAndClearBindings)},
- {const_cast<char*>("nativeExecute"), const_cast<char*>("(JJZ)V"),
- reinterpret_cast<void*>(nativeExecute)},
- {const_cast<char*>("nativeExecuteForLong"), const_cast<char*>("(JJ)J"),
- reinterpret_cast<void*>(nativeExecuteForLong)},
- {const_cast<char*>("nativeExecuteForString"),
- const_cast<char*>("(JJ)Ljava/lang/String;"),
- reinterpret_cast<void*>(nativeExecuteForString)},
- {const_cast<char*>("nativeExecuteForBlobFileDescriptor"),
- const_cast<char*>("(JJ)I"),
- reinterpret_cast<void*>(nativeExecuteForBlobFileDescriptor)},
- {const_cast<char*>("nativeExecuteForChangedRowCount"),
- const_cast<char*>("(JJ)I"),
- reinterpret_cast<void*>(nativeExecuteForChangedRowCount)},
- {const_cast<char*>("nativeExecuteForLastInsertedRowId"),
- const_cast<char*>("(JJ)J"),
- reinterpret_cast<void*>(nativeExecuteForLastInsertedRowId)},
- {const_cast<char*>("nativeExecuteForCursorWindow"),
- const_cast<char*>("(JJJIIZ)J"),
- reinterpret_cast<void*>(nativeExecuteForCursorWindow)},
- {const_cast<char*>("nativeGetDbLookaside"), const_cast<char*>("(J)I"),
- reinterpret_cast<void*>(nativeGetDbLookaside)},
- {const_cast<char*>("nativeCancel"), const_cast<char*>("(J)V"),
- reinterpret_cast<void*>(nativeCancel)},
- {const_cast<char*>("nativeResetCancel"), const_cast<char*>("(JZ)V"),
- reinterpret_cast<void*>(nativeResetCancel)},
-};
-
-int register_android_database_SQLiteConnection(JNIEnv* env) {
- static const char* kSQLiteClass =
- "org/robolectric/nativeruntime/SQLiteConnectionNatives";
- ScopedLocalRef<jclass> cls(env, env->FindClass(kSQLiteClass));
-
- if (cls.get() == nullptr) {
- ALOGE("jni SQLiteConnection registration failure, class not found '%s'",
- kSQLiteClass);
- return JNI_ERR;
- }
-
- jclass unaryClazz = env->FindClass("java/util/function/UnaryOperator");
- gUnaryOperator.apply = env->GetMethodID(
- unaryClazz, "apply", "(Ljava/lang/Object;)Ljava/lang/Object;");
-
- jclass binaryClazz = env->FindClass("java/util/function/BinaryOperator");
- gBinaryOperator.apply = env->GetMethodID(
- binaryClazz, "apply",
- "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
-
- const jint count = sizeof(sMethods) / sizeof(sMethods[0]);
- int status = env->RegisterNatives(cls.get(), sMethods, count);
- if (status < 0) {
- ALOGE("jni SQLite registration failure, status: %d", status);
- return JNI_ERR;
- }
-
- return JNI_VERSION_1_4;
-}
-}; // namespace android
diff --git a/nativeruntime/cpp/libcutils/CMakeLists.txt b/nativeruntime/cpp/libcutils/CMakeLists.txt
deleted file mode 100644
index 0c71a0a64..000000000
--- a/nativeruntime/cpp/libcutils/CMakeLists.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-cmake_minimum_required(VERSION 3.10)
-
-project(libcutils)
-
-include_directories(include)
-include_directories(../base/include)
-
-add_library(cutils STATIC ashmem.cpp)
diff --git a/nativeruntime/cpp/libcutils/ashmem.cpp b/nativeruntime/cpp/libcutils/ashmem.cpp
deleted file mode 100644
index 1b1fd7144..000000000
--- a/nativeruntime/cpp/libcutils/ashmem.cpp
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libcutils/ashmem-host.cpp
-
-#include "cutils/ashmem.h"
-
-/*
- * Implementation of the user-space ashmem API for the simulator, which lacks
- * an ashmem-enabled kernel. See ashmem-dev.c for the real ashmem-based version.
- */
-
-#include <errno.h>
-#include <fcntl.h>
-#include <limits.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <time.h>
-#include <unistd.h>
-
-// bionic and glibc both have TEMP_FAILURE_RETRY, but some (e.g. Mac OS) do not.
-#include "android-base/macros.h"
-
-static bool ashmem_validate_stat(int fd, struct stat* buf) {
- int result = fstat(fd, buf);
- if (result == -1) {
- return false;
- }
-
- /*
- * Check if this is an "ashmem" region.
- * TODO: This is very hacky, and can easily break.
- * We need some reliable indicator.
- */
- if (!(buf->st_nlink == 0 && S_ISREG(buf->st_mode))) {
- errno = ENOTTY;
- return false;
- }
- return true;
-}
-
-int ashmem_valid(int fd) {
- struct stat buf;
- return ashmem_validate_stat(fd, &buf);
-}
-
-int ashmem_create_region(const char* /*ignored*/, size_t size) {
- char pattern[PATH_MAX];
- snprintf(pattern, sizeof(pattern), "/tmp/android-ashmem-%d-XXXXXXXXX",
- getpid());
- int fd = mkstemp(pattern);
- if (fd == -1) return -1;
-
- unlink(pattern);
-
- if (TEMP_FAILURE_RETRY(ftruncate(fd, size)) == -1) {
- close(fd);
- return -1;
- }
-
- return fd;
-}
-
-int ashmem_set_prot_region(int /*fd*/, int /*prot*/) { return 0; }
-
-int ashmem_pin_region(int /*fd*/, size_t /*offset*/, size_t /*len*/) {
- return 0 /*ASHMEM_NOT_PURGED*/;
-}
-
-int ashmem_unpin_region(int /*fd*/, size_t /*offset*/, size_t /*len*/) {
- return 0 /*ASHMEM_IS_UNPINNED*/;
-}
-
-int ashmem_get_size_region(int fd) {
- struct stat buf;
- if (!ashmem_validate_stat(fd, &buf)) {
- return -1;
- }
-
- return buf.st_size;
-}
diff --git a/nativeruntime/cpp/libcutils/include/cutils/ashmem.h b/nativeruntime/cpp/libcutils/include/cutils/ashmem.h
deleted file mode 100644
index cc70b0a3d..000000000
--- a/nativeruntime/cpp/libcutils/include/cutils/ashmem.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/* cutils/ashmem.h
- **
- ** Copyright 2008 The Android Open Source Project
- **
- ** This file is dual licensed. It may be redistributed and/or modified
- ** under the terms of the Apache 2.0 License OR version 2 of the GNU
- ** General Public License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libcutils/include/cutils/ashmem.h
-
-#ifndef _CUTILS_ASHMEM_H
-#define _CUTILS_ASHMEM_H
-
-#include <stddef.h>
-
-#if defined(__BIONIC__)
-#include <linux/ashmem.h>
-#endif
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-int ashmem_valid(int fd);
-int ashmem_create_region(const char *name, size_t size);
-int ashmem_set_prot_region(int fd, int prot);
-int ashmem_pin_region(int fd, size_t offset, size_t len);
-int ashmem_unpin_region(int fd, size_t offset, size_t len);
-int ashmem_get_size_region(int fd);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* _CUTILS_ASHMEM_H */
diff --git a/nativeruntime/cpp/liblog/CMakeLists.txt b/nativeruntime/cpp/liblog/CMakeLists.txt
deleted file mode 100644
index 520d4e216..000000000
--- a/nativeruntime/cpp/liblog/CMakeLists.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-cmake_minimum_required(VERSION 3.10)
-
-project(log)
-
-add_library(log STATIC log.c)
diff --git a/nativeruntime/cpp/liblog/include/log/log.h b/nativeruntime/cpp/liblog/include/log/log.h
deleted file mode 100644
index 8bf9a92aa..000000000
--- a/nativeruntime/cpp/liblog/include/log/log.h
+++ /dev/null
@@ -1,78 +0,0 @@
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:device/generic/goldfish-opengl/fuchsia/include/cutils/log.h
-
-#include <stdint.h>
-
-#ifndef __CUTILS_LOG_H__
-#define __CUTILS_LOG_H__
-
-#ifndef LOG_TAG
-#define LOG_TAG nullptr
-#endif
-
-enum {
- ANDROID_LOG_UNKNOWN = 0,
- ANDROID_LOG_DEFAULT,
- ANDROID_LOG_VERBOSE,
- ANDROID_LOG_DEBUG,
- ANDROID_LOG_INFO,
- ANDROID_LOG_WARN,
- ANDROID_LOG_ERROR,
- ANDROID_LOG_FATAL,
- ANDROID_LOG_SILENT,
-};
-
-#define android_printLog(prio, tag, format, ...) \
- __android_log_print(prio, tag, "[prio %d] " format, prio, ##__VA_ARGS__)
-
-#define LOG_PRI(priority, tag, ...) android_printLog(priority, tag, __VA_ARGS__)
-#define ALOG(priority, tag, ...) LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__)
-
-#define __android_second(dummy, second, ...) second
-#define __android_rest(first, ...) , ##__VA_ARGS__
-
-#define android_printAssert(condition, tag, format, ...) \
- __android_log_assert(condition, tag, "assert: condition: %s " format, \
- condition, ##__VA_ARGS__)
-
-#define LOG_ALWAYS_FATAL_IF(condition, ...) \
- ((condition) \
- ? ((void)android_printAssert(#condition, LOG_TAG, ##__VA_ARGS__)) \
- : (void)0)
-
-#define LOG_ALWAYS_FATAL(...) \
- (((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__)))
-
-#define ALOGV(...) ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__))
-#define ALOGE(...) ((void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__))
-#define ALOGW(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__))
-#define ALOGD(...) ((void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__))
-
-#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__)
-
-#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__)
-
-#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__)
-
-#ifndef android_errorWriteLog
-#define android_errorWriteLog(tag, subTag) \
- __android_log_error_write(tag, subTag, -1, NULL, 0)
-#endif
-
-#ifndef android_errorWriteWithInfoLog
-#define android_errorWriteWithInfoLog(tag, subTag, uid, data, dataLen) \
- __android_log_error_write(tag, subTag, uid, data, dataLen)
-#endif
-
-extern "C" {
-
-int __android_log_print(int priority, const char* tag, const char* format, ...);
-
-[[noreturn]] void __android_log_assert(const char* condition, const char* tag,
- const char* format, ...);
-
-int __android_log_error_write(int tag, const char* subTag, int32_t uid,
- const char* data, uint32_t dataLen);
-}
-
-#endif
diff --git a/nativeruntime/cpp/liblog/log.c b/nativeruntime/cpp/liblog/log.c
deleted file mode 100644
index 271a2d405..000000000
--- a/nativeruntime/cpp/liblog/log.c
+++ /dev/null
@@ -1,35 +0,0 @@
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <stdarg.h>
-
-int __android_log_print(int prio, const char* tag, const char* fmt, ...) {
- ((void)prio);
- ((void)tag);
- ((void)fmt);
-
- if (prio >= 4) {
- va_list args;
- va_start(args, fmt);
- fprintf(stderr, "%s: ", tag);
- fprintf(stderr, fmt, args);
- va_end(args);
- }
- return 0;
-}
-
-int __android_log_error_write(int tag, const char* subTag, int32_t uid,
- const char* data, uint32_t dataLen) {
- ((void)tag);
- return 0;
-}
-
-void __android_log_assert(const char* condition, const char* tag,
- const char* format, ...) {
- va_list args;
- va_start(args, format);
- fprintf(stderr, "%s: ", tag);
- fprintf(stderr, format, args);
- va_end(args);
- abort();
-}
diff --git a/nativeruntime/cpp/libnativehelper/include/nativehelper/JNIHelp.h b/nativeruntime/cpp/libnativehelper/include/nativehelper/JNIHelp.h
deleted file mode 100644
index 1dd44b602..000000000
--- a/nativeruntime/cpp/libnativehelper/include/nativehelper/JNIHelp.h
+++ /dev/null
@@ -1,552 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/master:libnativehelper/include/nativehelper/JNIHelp.h
-
-/*
- * JNI helper functions.
- *
- * This file may be included by C or C++ code, which is trouble because jni.h
- * uses different typedefs for JNIEnv in each language.
- */
-#ifndef NATIVEHELPER_JNIHELP_H_
-#define NATIVEHELPER_JNIHELP_H_
-
-#include <errno.h>
-#include <jni.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/cdefs.h>
-#include <unistd.h>
-
-// #include <android/log.h>
-
-// Avoid formatting this as it must match webview's usage
-// (webview/graphics_utils.cpp).
-// clang-format off
-#ifndef NELEM
-#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
-#endif
-// clang-format on
-
-/*
- * For C++ code, we provide inlines that map to the C functions. g++ always
- * inlines these, even on non-optimized builds.
- */
-#if defined(__cplusplus)
-
-namespace android::jnihelp {
-struct [[maybe_unused]] ExpandableString {
- size_t dataSize; // The length of the C string data (not including the
- // null-terminator).
- char* data; // The C string data.
-};
-
-[[maybe_unused]] static void ExpandableStringInitialize(
- struct ExpandableString* s) {
- memset(s, 0, sizeof(*s));
-}
-
-[[maybe_unused]] static void ExpandableStringRelease(
- struct ExpandableString* s) {
- free(s->data);
- memset(s, 0, sizeof(*s));
-}
-
-[[maybe_unused]] static bool ExpandableStringAppend(struct ExpandableString* s,
- const char* text) {
- size_t textSize = strlen(text);
- size_t requiredSize = s->dataSize + textSize + 1;
- char* data = static_cast<char*>(realloc(s->data, requiredSize));
- if (data == nullptr) {
- return false;
- }
- s->data = data;
- memcpy(s->data + s->dataSize, text, textSize + 1);
- s->dataSize += textSize;
- return true;
-}
-
-[[maybe_unused]] static bool ExpandableStringAssign(struct ExpandableString* s,
- const char* text) {
- ExpandableStringRelease(s);
- return ExpandableStringAppend(s, text);
-}
-
-[[maybe_unused]] inline const char* platformStrError(int errnum, char* buf,
- size_t buflen) {
-#ifdef _WIN32
- strerror_s(buf, buflen, errnum);
- return buf;
-#elif defined(__USE_GNU)
- // char *strerror_r(int errnum, char *buf, size_t buflen); /* GNU-specific */
- return strerror_r(errnum, buf, buflen);
-#else
- // int strerror_r(int errnum, char *buf, size_t buflen); /* XSI-compliant */
- int rc = strerror_r(errnum, buf, buflen);
- if (rc != 0) {
- snprintf(buf, buflen, "errno %d", errnum);
- }
- return buf;
-#endif
-}
-
-[[maybe_unused]] static jmethodID FindMethod(JNIEnv* env, const char* className,
- const char* methodName,
- const char* descriptor) {
- // This method is only valid for classes in the core library which are
- // not unloaded during the lifetime of managed code execution.
- jclass clazz = env->FindClass(className);
- jmethodID methodId = env->GetMethodID(clazz, methodName, descriptor);
- env->DeleteLocalRef(clazz);
- return methodId;
-}
-
-[[maybe_unused]] static bool AppendJString(JNIEnv* env, jstring text,
- struct ExpandableString* dst) {
- const char* utfText = env->GetStringUTFChars(text, nullptr);
- if (utfText == nullptr) {
- return false;
- }
- bool success = ExpandableStringAppend(dst, utfText);
- env->ReleaseStringUTFChars(text, utfText);
- return success;
-}
-
-/*
- * Returns a human-readable summary of an exception object. The buffer will
- * be populated with the "binary" class name and, if present, the
- * exception message.
- */
-[[maybe_unused]] static bool GetExceptionSummary(JNIEnv* env, jthrowable thrown,
- struct ExpandableString* dst) {
- // Summary is <exception_class_name> ": " <exception_message>
- jclass exceptionClass = env->GetObjectClass(thrown); // Always succeeds
- jmethodID getName =
- FindMethod(env, "java/lang/Class", "getName", "()Ljava/lang/String;");
- jstring className = (jstring)env->CallObjectMethod(exceptionClass, getName);
- if (className == nullptr) {
- ExpandableStringAssign(dst, "<error getting class name>");
- env->ExceptionClear();
- env->DeleteLocalRef(exceptionClass);
- return false;
- }
- env->DeleteLocalRef(exceptionClass);
- exceptionClass = nullptr;
-
- if (!AppendJString(env, className, dst)) {
- ExpandableStringAssign(dst, "<error getting class name UTF-8>");
- env->ExceptionClear();
- env->DeleteLocalRef(className);
- return false;
- }
- env->DeleteLocalRef(className);
- className = nullptr;
-
- jmethodID getMessage = FindMethod(env, "java/lang/Throwable", "getMessage",
- "()Ljava/lang/String;");
- jstring message = (jstring)env->CallObjectMethod(thrown, getMessage);
- if (message == nullptr) {
- return true;
- }
-
- bool success =
- (ExpandableStringAppend(dst, ": ") && AppendJString(env, message, dst));
- if (!success) {
- // Two potential reasons for reaching here:
- //
- // 1. managed heap allocation failure (OOME).
- // 2. native heap allocation failure for the storage in |dst|.
- //
- // Attempt to append failure notification, okay to fail, |dst| contains the
- // class name of |thrown|.
- ExpandableStringAppend(dst, "<error getting message>");
- // Clear OOME if present.
- env->ExceptionClear();
- }
- env->DeleteLocalRef(message);
- message = nullptr;
- return success;
-}
-
-[[maybe_unused]] static jobject NewStringWriter(JNIEnv* env) {
- jclass clazz = env->FindClass("java/io/StringWriter");
- jmethodID init = env->GetMethodID(clazz, "<init>", "()V");
- jobject instance = env->NewObject(clazz, init);
- env->DeleteLocalRef(clazz);
- return instance;
-}
-
-[[maybe_unused]] static jstring StringWriterToString(JNIEnv* env,
- jobject stringWriter) {
- jmethodID toString = FindMethod(env, "java/io/StringWriter", "toString",
- "()Ljava/lang/String;");
- return (jstring)env->CallObjectMethod(stringWriter, toString);
-}
-
-[[maybe_unused]] static jobject NewPrintWriter(JNIEnv* env, jobject writer) {
- jclass clazz = env->FindClass("java/io/PrintWriter");
- jmethodID init = env->GetMethodID(clazz, "<init>", "(Ljava/io/Writer;)V");
- jobject instance = env->NewObject(clazz, init, writer);
- env->DeleteLocalRef(clazz);
- return instance;
-}
-
-[[maybe_unused]] static bool GetStackTrace(JNIEnv* env, jthrowable thrown,
- struct ExpandableString* dst) {
- // This function is equivalent to the following Java snippet:
- // StringWriter sw = new StringWriter();
- // PrintWriter pw = new PrintWriter(sw);
- // thrown.printStackTrace(pw);
- // String trace = sw.toString();
- // return trace;
- jobject sw = NewStringWriter(env);
- if (sw == nullptr) {
- return false;
- }
-
- jobject pw = NewPrintWriter(env, sw);
- if (pw == nullptr) {
- env->DeleteLocalRef(sw);
- return false;
- }
-
- jmethodID printStackTrace =
- FindMethod(env, "java/lang/Throwable", "printStackTrace",
- "(Ljava/io/PrintWriter;)V");
- env->CallVoidMethod(thrown, printStackTrace, pw);
-
- jstring trace = StringWriterToString(env, sw);
-
- env->DeleteLocalRef(pw);
- pw = nullptr;
- env->DeleteLocalRef(sw);
- sw = nullptr;
-
- if (trace == nullptr) {
- return false;
- }
-
- bool success = AppendJString(env, trace, dst);
- env->DeleteLocalRef(trace);
- return success;
-}
-
-[[maybe_unused]] static void GetStackTraceOrSummary(
- JNIEnv* env, jthrowable thrown, struct ExpandableString* dst) {
- // This method attempts to get a stack trace or summary info for an exception.
- // The exception may be provided in the |thrown| argument to this function.
- // If |thrown| is NULL, then any pending exception is used if it exists.
-
- // Save pending exception, callees may raise other exceptions. Any pending
- // exception is rethrown when this function exits.
- jthrowable pendingException = env->ExceptionOccurred();
- if (pendingException != nullptr) {
- env->ExceptionClear();
- }
-
- if (thrown == nullptr) {
- if (pendingException == nullptr) {
- ExpandableStringAssign(dst, "<no pending exception>");
- return;
- }
- thrown = pendingException;
- }
-
- if (!GetStackTrace(env, thrown, dst)) {
- // GetStackTrace may have raised an exception, clear it since it's not for
- // the caller.
- env->ExceptionClear();
- GetExceptionSummary(env, thrown, dst);
- }
-
- if (pendingException != nullptr) {
- // Re-throw the pending exception present when this method was called.
- env->Throw(pendingException);
- env->DeleteLocalRef(pendingException);
- }
-}
-
-[[maybe_unused]] static void DiscardPendingException(JNIEnv* env,
- const char* className) {
- jthrowable exception = env->ExceptionOccurred();
- env->ExceptionClear();
- if (exception == nullptr) {
- return;
- }
-
- struct ExpandableString summary;
- ExpandableStringInitialize(&summary);
- GetExceptionSummary(env, exception, &summary);
- // const char* details = (summary.data != NULL) ? summary.data : "Unknown";
- // __android_log_print(ANDROID_LOG_WARN, "JNIHelp",
- // "Discarding pending exception (%s) to throw %s",
- // details, className);
- ExpandableStringRelease(&summary);
- env->DeleteLocalRef(exception);
-}
-
-[[maybe_unused]] static int ThrowException(JNIEnv* env, const char* className,
- const char* ctorSig, ...) {
- int status = -1;
- jclass exceptionClass = nullptr;
-
- va_list args;
- va_start(args, ctorSig);
-
- DiscardPendingException(env, className);
-
- {
- /* We want to clean up local references before returning from this function,
- * so, regardless of return status, the end block must run. Have the work
- * done in a
- * nested block to avoid using any uninitialized variables in the end block.
- */
- exceptionClass = env->FindClass(className);
- if (exceptionClass == nullptr) {
- // __android_log_print(ANDROID_LOG_ERROR, "JNIHelp", "Unable to find
- // exception class %s",
- // className);
- /* an exception, most likely ClassNotFoundException, will now be pending
- */
- goto end;
- }
-
- jmethodID init = env->GetMethodID(exceptionClass, "<init>", ctorSig);
- if (init == nullptr) {
- // __android_log_print(ANDROID_LOG_ERROR, "JNIHelp",
- // "Failed to find constructor for '%s' '%s'",
- // className, ctorSig);
- goto end;
- }
-
- jobject instance = env->NewObjectV(exceptionClass, init, args);
- if (instance == nullptr) {
- // __android_log_print(ANDROID_LOG_ERROR, "JNIHelp", "Failed to
- // construct '%s'",
- // className);
- goto end;
- }
-
- if (env->Throw((jthrowable)instance) != JNI_OK) {
- // __android_log_print(ANDROID_LOG_ERROR, "JNIHelp", "Failed to throw
- // '%s'", className);
- /* an exception, most likely OOM, will now be pending */
- goto end;
- }
-
- /* everything worked fine, just update status to success and clean up */
- status = 0;
- }
-
-end:
- va_end(args);
- if (exceptionClass != nullptr) {
- env->DeleteLocalRef(exceptionClass);
- }
- return status;
-}
-
-[[maybe_unused]] static jstring CreateExceptionMsg(JNIEnv* env,
- const char* msg) {
- jstring detailMessage = env->NewStringUTF(msg);
- if (detailMessage == nullptr) {
- /* Not really much we can do here. We're probably dead in the water,
- but let's try to stumble on... */
- env->ExceptionClear();
- }
- return detailMessage;
-}
-} // namespace android::jnihelp
-
-/*
- * Register one or more native methods with a particular class. "className"
- * looks like "java/lang/String". Aborts on failure, returns 0 on success.
- */
-[[maybe_unused]] static int jniRegisterNativeMethods(
- JNIEnv* env, const char* className, const JNINativeMethod* methods,
- int numMethods) {
- using namespace android::jnihelp;
- jclass clazz = env->FindClass(className);
- if (clazz == nullptr) {
- // __android_log_assert("clazz == NULL", "JNIHelp",
- // "Native registration unable to find class '%s';
- // aborting...", className);
- }
- int result = env->RegisterNatives(clazz, methods, numMethods);
- env->DeleteLocalRef(clazz);
- if (result == 0) {
- return 0;
- }
-
- // Failure to register natives is fatal. Try to report the corresponding
- // exception, otherwise abort with generic failure message.
- jthrowable thrown = env->ExceptionOccurred();
- if (thrown != nullptr) {
- struct ExpandableString summary;
- ExpandableStringInitialize(&summary);
- if (GetExceptionSummary(env, thrown, &summary)) {
- // __android_log_print(ANDROID_LOG_FATAL, "JNIHelp", "%s",
- // summary.data);
- }
- ExpandableStringRelease(&summary);
- env->DeleteLocalRef(thrown);
- }
- // __android_log_print(ANDROID_LOG_FATAL, "JNIHelp",
- // "RegisterNatives failed for '%s'; aborting...",
- // className);
- return result;
-}
-
-/*
- * Throw an exception with the specified class and an optional message.
- *
- * The "className" argument will be passed directly to FindClass, which
- * takes strings with slashes (e.g. "java/lang/Object").
- *
- * If an exception is currently pending, we log a warning message and
- * clear it.
- *
- * Returns 0 on success, nonzero if something failed (e.g. the exception
- * class couldn't be found, so *an* exception will still be pending).
- *
- * Currently aborts the VM if it can't throw the exception.
- */
-[[maybe_unused]] static int jniThrowException(JNIEnv* env,
- const char* className,
- const char* msg) {
- using namespace android::jnihelp;
- jstring _detailMessage = CreateExceptionMsg(env, msg);
- int _status =
- ThrowException(env, className, "(Ljava/lang/String;)V", _detailMessage);
- if (_detailMessage != nullptr) {
- env->DeleteLocalRef(_detailMessage);
- }
- return _status;
-}
-
-/*
- * Throw an android.system.ErrnoException, with the given function name and
- * errno value.
- */
-[[maybe_unused]] static int jniThrowErrnoException(JNIEnv* env,
- const char* functionName,
- int errnum) {
- using namespace android::jnihelp;
- jstring _detailMessage = CreateExceptionMsg(env, functionName);
- int _status =
- ThrowException(env, "android/system/ErrnoException",
- "(Ljava/lang/String;I)V", _detailMessage, errnum);
- if (_detailMessage != nullptr) {
- env->DeleteLocalRef(_detailMessage);
- }
- return _status;
-}
-
-/*
- * Throw an exception with the specified class and formatted error message.
- *
- * The "className" argument will be passed directly to FindClass, which
- * takes strings with slashes (e.g. "java/lang/Object").
- *
- * If an exception is currently pending, we log a warning message and
- * clear it.
- *
- * Returns 0 on success, nonzero if something failed (e.g. the exception
- * class couldn't be found, so *an* exception will still be pending).
- *
- * Currently aborts the VM if it can't throw the exception.
- */
-[[maybe_unused]] static int jniThrowExceptionFmt(JNIEnv* env,
- const char* className,
- const char* fmt, ...) {
- va_list args;
- va_start(args, fmt);
- char msgBuf[512];
- vsnprintf(msgBuf, sizeof(msgBuf), fmt, args);
- va_end(args);
- return jniThrowException(env, className, msgBuf);
-}
-
-[[maybe_unused]] static int jniThrowNullPointerException(JNIEnv* env,
- const char* msg) {
- return jniThrowException(env, "java/lang/NullPointerException", msg);
-}
-
-[[maybe_unused]] static int jniThrowRuntimeException(JNIEnv* env,
- const char* msg) {
- return jniThrowException(env, "java/lang/RuntimeException", msg);
-}
-
-[[maybe_unused]] static int jniThrowIOException(JNIEnv* env, int errno_value) {
- using namespace android::jnihelp;
- char buffer[80];
- const char* message = platformStrError(errno_value, buffer, sizeof(buffer));
- return jniThrowException(env, "java/io/IOException", message);
-}
-
-/*
- * Returns a Java String object created from UTF-16 data either from jchar or,
- * if called from C++11, char16_t (a bitwise identical distinct type).
- */
-[[maybe_unused]] static inline jstring jniCreateString(
- JNIEnv* env, const jchar* unicodeChars, jsize len) {
- return env->NewString(unicodeChars, len);
-}
-
-[[maybe_unused]] static inline jstring jniCreateString(
- JNIEnv* env, const char16_t* unicodeChars, jsize len) {
- return jniCreateString(env, reinterpret_cast<const jchar*>(unicodeChars),
- len);
-}
-
-/*
- * Log a message and an exception.
- * If exception is NULL, logs the current exception in the JNI environment.
- */
-[[maybe_unused]] static void jniLogException(JNIEnv* env, int priority,
- const char* tag,
- jthrowable exception = nullptr) {
- using namespace android::jnihelp;
- struct ExpandableString summary;
- ExpandableStringInitialize(&summary);
- GetStackTraceOrSummary(env, exception, &summary);
- // const char* details = (summary.data != NULL) ? summary.data : "No memory
- // to report exception";
- // __android_log_write(priority, tag, details);
- ExpandableStringRelease(&summary);
-}
-
-#else // defined(__cplusplus)
-
-// ART-internal only methods (not exported), exposed for legacy C users
-
-int jniRegisterNativeMethods(JNIEnv* env, const char* className,
- const JNINativeMethod* gMethods, int numMethods);
-
-void jniLogException(JNIEnv* env, int priority, const char* tag,
- jthrowable thrown);
-
-int jniThrowException(JNIEnv* env, const char* className, const char* msg);
-
-int jniThrowNullPointerException(JNIEnv* env, const char* msg);
-
-#endif // defined(__cplusplus)
-
-#endif // NATIVEHELPER_JNIHELP_H_
diff --git a/nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_local_ref.h b/nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_local_ref.h
deleted file mode 100644
index 53e264449..000000000
--- a/nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_local_ref.h
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:libnativehelper/header_only_include/nativehelper/scoped_local_ref.h
-
-#ifndef LIBNATIVEHELPER_HEADER_ONLY_INCLUDE_NATIVEHELPER_SCOPED_LOCAL_REF_H_
-#define LIBNATIVEHELPER_HEADER_ONLY_INCLUDE_NATIVEHELPER_SCOPED_LOCAL_REF_H_
-
-#include <android-base/macros.h>
-
-#include <cstddef>
-
-#include "jni.h"
-// #include "nativehelper_utils.h"
-
-// A smart pointer that deletes a JNI local reference when it goes out of scope.
-template <typename T>
-class ScopedLocalRef {
- public:
- ScopedLocalRef(JNIEnv* env, T localRef) : mEnv(env), mLocalRef(localRef) {}
-
- ScopedLocalRef(ScopedLocalRef&& s) noexcept
- : mEnv(s.mEnv), mLocalRef(s.release()) {}
-
- explicit ScopedLocalRef(JNIEnv* env) : mEnv(env), mLocalRef(nullptr) {}
-
- ~ScopedLocalRef() { reset(); }
-
- void reset(T ptr = NULL) {
- if (ptr != mLocalRef) {
- if (mLocalRef != NULL) {
- mEnv->DeleteLocalRef(mLocalRef);
- }
- mLocalRef = ptr;
- }
- }
-
- T release() __attribute__((warn_unused_result)) {
- T localRef = mLocalRef;
- mLocalRef = NULL;
- return localRef;
- }
-
- T get() const { return mLocalRef; }
-
- // We do not expose an empty constructor as it can easily lead to errors
- // using common idioms, e.g.:
- // ScopedLocalRef<...> ref;
- // ref.reset(...);
-
- // Move assignment operator.
- ScopedLocalRef& operator=(ScopedLocalRef&& s) noexcept {
- reset(s.release());
- mEnv = s.mEnv;
- return *this;
- }
-
- // Allows "if (scoped_ref == nullptr)"
- bool operator==(std::nullptr_t) const { return mLocalRef == nullptr; }
-
- // Allows "if (scoped_ref != nullptr)"
- bool operator!=(std::nullptr_t) const { return mLocalRef != nullptr; }
-
- private:
- JNIEnv* mEnv;
- T mLocalRef;
-
- DISALLOW_COPY_AND_ASSIGN(ScopedLocalRef);
-};
-
-#endif // LIBNATIVEHELPER_HEADER_ONLY_INCLUDE_NATIVEHELPER_SCOPED_LOCAL_REF_H_
diff --git a/nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_utf8_chars.h b/nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_utf8_chars.h
deleted file mode 100644
index e2be8ef63..000000000
--- a/nativeruntime/cpp/libnativehelper/include/nativehelper/scoped_utf8_chars.h
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:libnativehelper/header_only_include/nativehelper/scoped_utf_chars.h
-
-#ifndef LIBNATIVEHELPER_HEADER_ONLY_INCLUDE_NATIVEHELPER_SCOPED_UTF_CHARS_H_
-#define LIBNATIVEHELPER_HEADER_ONLY_INCLUDE_NATIVEHELPER_SCOPED_UTF_CHARS_H_
-
-#include <android-base/macros.h>
-#include <string.h>
-
-#include "JNIHelp.h"
-#include "jni.h"
-// #include "nativehelper_utils.h"
-
-// A smart pointer that provides read-only access to a Java string's UTF chars.
-// Unlike GetStringUTFChars, we throw NullPointerException rather than abort if
-// passed a null jstring, and c_str will return nullptr.
-// This makes the correct idiom very simple:
-//
-// ScopedUtfChars name(env, java_name);
-// if (name.c_str() == nullptr) {
-// return nullptr;
-// }
-class ScopedUtfChars {
- public:
- ScopedUtfChars(JNIEnv* env, jstring s) : env_(env), string_(s) {
- if (s == nullptr) {
- utf_chars_ = nullptr;
- jniThrowNullPointerException(env, "null");
- } else {
- utf_chars_ = env->GetStringUTFChars(s, nullptr);
- }
- }
-
- ScopedUtfChars(ScopedUtfChars&& rhs) noexcept
- : env_(rhs.env_), string_(rhs.string_), utf_chars_(rhs.utf_chars_) {
- rhs.env_ = nullptr;
- rhs.string_ = nullptr;
- rhs.utf_chars_ = nullptr;
- }
-
- ~ScopedUtfChars() {
- if (utf_chars_) {
- env_->ReleaseStringUTFChars(string_, utf_chars_);
- }
- }
-
- ScopedUtfChars& operator=(ScopedUtfChars&& rhs) noexcept {
- if (this != &rhs) {
- // Delete the currently owned UTF chars.
- this->~ScopedUtfChars();
-
- // Move the rhs ScopedUtfChars and zero it out.
- env_ = rhs.env_;
- string_ = rhs.string_;
- utf_chars_ = rhs.utf_chars_;
- rhs.env_ = nullptr;
- rhs.string_ = nullptr;
- rhs.utf_chars_ = nullptr;
- }
- return *this;
- }
-
- const char* c_str() const { return utf_chars_; }
-
- size_t size() const { return strlen(utf_chars_); }
-
- const char& operator[](size_t n) const { return utf_chars_[n]; }
-
- private:
- JNIEnv* env_;
- jstring string_;
- const char* utf_chars_;
-
- DISALLOW_COPY_AND_ASSIGN(ScopedUtfChars);
-};
-
-#endif // LIBNATIVEHELPER_HEADER_ONLY_INCLUDE_NATIVEHELPER_SCOPED_UTF_CHARS_H_
diff --git a/nativeruntime/cpp/libutils/CMakeLists.txt b/nativeruntime/cpp/libutils/CMakeLists.txt
deleted file mode 100644
index 46251c527..000000000
--- a/nativeruntime/cpp/libutils/CMakeLists.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-cmake_minimum_required(VERSION 3.10)
-# Some libutils headers require C++17
-set (CMAKE_CXX_STANDARD 17)
-
-project(utils)
-
-include_directories(../base/include)
-include_directories(../liblog/include)
-include_directories(include)
-
-add_library(utils STATIC
- include/utils/String16.h
- SharedBuffer.cpp
- Unicode.cpp
- String8.cpp
- String16.cpp
-)
diff --git a/nativeruntime/cpp/libutils/SharedBuffer.cpp b/nativeruntime/cpp/libutils/SharedBuffer.cpp
deleted file mode 100644
index 4e35bec6d..000000000
--- a/nativeruntime/cpp/libutils/SharedBuffer.cpp
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/SharedBuffer.cpp
-
-#define LOG_TAG "sharedbuffer"
-
-#include "SharedBuffer.h"
-
-#include <log/log.h>
-#include <stdlib.h>
-#include <string.h>
-
-// ---------------------------------------------------------------------------
-
-namespace android {
-
-SharedBuffer* SharedBuffer::alloc(size_t size) {
- // Don't overflow if the combined size of the buffer / header is larger than
- // size_max.
- LOG_ALWAYS_FATAL_IF((size >= (SIZE_MAX - sizeof(SharedBuffer))),
- "Invalid buffer size %zu", size);
-
- SharedBuffer* sb =
- static_cast<SharedBuffer*>(malloc(sizeof(SharedBuffer) + size));
- if (sb) {
- // Should be std::atomic_init(&sb->mRefs, 1);
- // But that generates a warning with some compilers.
- // The following is OK on Android-supported platforms.
- sb->mRefs.store(1, std::memory_order_relaxed);
- sb->mSize = size;
- sb->mClientMetadata = 0;
- }
- return sb;
-}
-
-void SharedBuffer::dealloc(const SharedBuffer* released) {
- free(const_cast<SharedBuffer*>(released));
-}
-
-SharedBuffer* SharedBuffer::edit() const {
- if (onlyOwner()) {
- return const_cast<SharedBuffer*>(this);
- }
- SharedBuffer* sb = alloc(mSize);
- if (sb) {
- memcpy(sb->data(), data(), size());
- release();
- }
- return sb;
-}
-
-SharedBuffer* SharedBuffer::editResize(size_t newSize) const {
- if (onlyOwner()) {
- SharedBuffer* buf = const_cast<SharedBuffer*>(this);
- if (buf->mSize == newSize) return buf;
- // Don't overflow if the combined size of the new buffer / header is larger
- // than size_max.
- LOG_ALWAYS_FATAL_IF((newSize >= (SIZE_MAX - sizeof(SharedBuffer))),
- "Invalid buffer size %zu", newSize);
-
- buf = static_cast<SharedBuffer*>(
- realloc(buf, sizeof(SharedBuffer) + newSize));
- if (buf != nullptr) {
- buf->mSize = newSize;
- return buf;
- }
- }
- SharedBuffer* sb = alloc(newSize);
- if (sb) {
- const size_t mySize = mSize;
- memcpy(sb->data(), data(), newSize < mySize ? newSize : mySize);
- release();
- }
- return sb;
-}
-
-SharedBuffer* SharedBuffer::attemptEdit() const {
- if (onlyOwner()) {
- return const_cast<SharedBuffer*>(this);
- }
- return nullptr;
-}
-
-SharedBuffer* SharedBuffer::reset(size_t new_size) const {
- // cheap-o-reset.
- SharedBuffer* sb = alloc(new_size);
- if (sb) {
- release();
- }
- return sb;
-}
-
-void SharedBuffer::acquire() const {
- mRefs.fetch_add(1, std::memory_order_relaxed);
-}
-
-int32_t SharedBuffer::release(uint32_t flags) const {
- const bool useDealloc = ((flags & eKeepStorage) == 0);
- if (onlyOwner()) {
- // Since we're the only owner, our reference count goes to zero.
- mRefs.store(0, std::memory_order_relaxed);
- if (useDealloc) {
- dealloc(this);
- }
- // As the only owner, our previous reference count was 1.
- return 1;
- }
- // There's multiple owners, we need to use an atomic decrement.
- int32_t prevRefCount = mRefs.fetch_sub(1, std::memory_order_release);
- if (prevRefCount == 1) {
- // We're the last reference, we need the acquire fence.
- std::atomic_thread_fence(std::memory_order_acquire);
- if (useDealloc) {
- dealloc(this);
- }
- }
- return prevRefCount;
-}
-
-}; // namespace android
diff --git a/nativeruntime/cpp/libutils/SharedBuffer.h b/nativeruntime/cpp/libutils/SharedBuffer.h
deleted file mode 100644
index dc5d504af..000000000
--- a/nativeruntime/cpp/libutils/SharedBuffer.h
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/SharedBuffer.h
-
-/*
- * DEPRECATED. DO NOT USE FOR NEW CODE.
- */
-
-#ifndef ANDROID_SHARED_BUFFER_H
-#define ANDROID_SHARED_BUFFER_H
-
-#include <stdint.h>
-#include <sys/types.h>
-
-#include <atomic>
-
-// ---------------------------------------------------------------------------
-
-namespace android {
-
-class SharedBuffer {
- public:
- /* flags to use with release() */
- enum { eKeepStorage = 0x00000001 };
-
- /*! allocate a buffer of size 'size' and acquire() it.
- * call release() to free it.
- */
- static SharedBuffer* alloc(size_t size);
-
- /*! free the memory associated with the SharedBuffer.
- * Fails if there are any users associated with this SharedBuffer.
- * In other words, the buffer must have been release by all its
- * users.
- */
- static void dealloc(const SharedBuffer* released);
-
- //! access the data for read
- inline const void* data() const;
-
- //! access the data for read/write
- inline void* data();
-
- //! get size of the buffer
- inline size_t size() const;
-
- //! get back a SharedBuffer object from its data
- static inline SharedBuffer* bufferFromData(void* data);
-
- //! get back a SharedBuffer object from its data
- static inline const SharedBuffer* bufferFromData(const void* data);
-
- //! get the size of a SharedBuffer object from its data
- static inline size_t sizeFromData(const void* data);
-
- //! edit the buffer (get a writtable, or non-const, version of it)
- SharedBuffer* edit() const;
-
- //! edit the buffer, resizing if needed
- SharedBuffer* editResize(size_t size) const;
-
- //! like edit() but fails if a copy is required
- SharedBuffer* attemptEdit() const;
-
- //! resize and edit the buffer, loose its content.
- SharedBuffer* reset(size_t size) const;
-
- //! acquire/release a reference on this buffer
- void acquire() const;
-
- /*! release a reference on this buffer, with the option of not
- * freeing the memory associated with it if it was the last reference
- * returns the previous reference count
- */
- int32_t release(uint32_t flags = 0) const;
-
- //! returns whether or not we're the only owner
- inline bool onlyOwner() const;
-
- private:
- inline SharedBuffer() {}
- inline ~SharedBuffer() {}
- SharedBuffer(const SharedBuffer&);
- SharedBuffer& operator=(const SharedBuffer&);
-
- // Must be sized to preserve correct alignment.
- mutable std::atomic<int32_t> mRefs;
- size_t mSize;
- uint32_t mReserved;
-
- public:
- // mClientMetadata is reserved for client use. It is initialized to 0
- // and the clients can do whatever they want with it. Note that this is
- // placed last so that it is adjcent to the buffer allocated.
- uint32_t mClientMetadata;
-};
-
-static_assert(sizeof(SharedBuffer) % 8 == 0 &&
- (sizeof(size_t) > 4 || sizeof(SharedBuffer) == 16),
- "SharedBuffer has unexpected size");
-
-// ---------------------------------------------------------------------------
-
-const void* SharedBuffer::data() const { return this + 1; }
-
-void* SharedBuffer::data() { return this + 1; }
-
-size_t SharedBuffer::size() const { return mSize; }
-
-SharedBuffer* SharedBuffer::bufferFromData(void* data) {
- return data ? static_cast<SharedBuffer*>(data) - 1 : nullptr;
-}
-
-const SharedBuffer* SharedBuffer::bufferFromData(const void* data) {
- return data ? static_cast<const SharedBuffer*>(data) - 1 : nullptr;
-}
-
-size_t SharedBuffer::sizeFromData(const void* data) {
- return data ? bufferFromData(data)->mSize : 0;
-}
-
-bool SharedBuffer::onlyOwner() const {
- return (mRefs.load(std::memory_order_acquire) == 1);
-}
-
-} // namespace android
-
-// ---------------------------------------------------------------------------
-
-#endif // ANDROID_VECTOR_H
diff --git a/nativeruntime/cpp/libutils/String16.cpp b/nativeruntime/cpp/libutils/String16.cpp
deleted file mode 100644
index 460b19c11..000000000
--- a/nativeruntime/cpp/libutils/String16.cpp
+++ /dev/null
@@ -1,450 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/String16.cpp
-
-#include <ctype.h>
-#include <log/log.h>
-#include <utils/String16.h>
-
-#include "SharedBuffer.h"
-
-namespace android {
-
-static const StaticString16 emptyString(u"");
-static inline char16_t* getEmptyString() {
- return const_cast<char16_t*>(emptyString.string());
-}
-
-// ---------------------------------------------------------------------------
-
-void* String16::alloc(size_t size) {
- SharedBuffer* buf = SharedBuffer::alloc(size);
- buf->mClientMetadata = kIsSharedBufferAllocated;
- return buf;
-}
-
-char16_t* String16::allocFromUTF8(const char* u8str, size_t u8len) {
- if (u8len == 0) return getEmptyString();
-
- const uint8_t* u8cur = reinterpret_cast<const uint8_t*>(u8str);
-
- const ssize_t u16len = utf8_to_utf16_length(u8cur, u8len);
- if (u16len < 0) {
- return getEmptyString();
- }
-
- SharedBuffer* buf =
- static_cast<SharedBuffer*>(alloc(sizeof(char16_t) * (u16len + 1)));
- if (buf) {
- u8cur = reinterpret_cast<const uint8_t*>(u8str);
- char16_t* u16str = static_cast<char16_t*>(buf->data());
-
- utf8_to_utf16(u8cur, u8len, u16str, (static_cast<size_t>(u16len)) + 1);
-
- // printf("Created UTF-16 string from UTF-8 \"%s\":", in);
- // printHexData(1, str, buf->size(), 16, 1);
- // printf("\n");
-
- return u16str;
- }
-
- return getEmptyString();
-}
-
-char16_t* String16::allocFromUTF16(const char16_t* u16str, size_t u16len) {
- if (u16len >= SIZE_MAX / sizeof(char16_t)) {
- android_errorWriteLog(0x534e4554, "73826242");
- abort();
- }
-
- SharedBuffer* buf =
- static_cast<SharedBuffer*>(alloc((u16len + 1) * sizeof(char16_t)));
- ALOG_ASSERT(buf, "Unable to allocate shared buffer");
- if (buf) {
- char16_t* str = static_cast<char16_t*>(buf->data());
- memcpy(str, u16str, u16len * sizeof(char16_t));
- str[u16len] = 0;
- return str;
- }
- return getEmptyString();
-}
-
-// ---------------------------------------------------------------------------
-
-String16::String16() : mString(getEmptyString()) {}
-
-String16::String16(StaticLinkage) : mString(nullptr) {
- // this constructor is used when we can't rely on the static-initializers
- // having run. In this case we always allocate an empty string. It's less
- // efficient than using getEmptyString(), but we assume it's uncommon.
-
- SharedBuffer* buf = static_cast<SharedBuffer*>(alloc(sizeof(char16_t)));
- char16_t* data = static_cast<char16_t*>(buf->data());
- data[0] = 0;
- mString = data;
-}
-
-String16::String16(const String16& o) : mString(o.mString) { acquire(); }
-
-String16::String16(const String16& o, size_t len, size_t begin)
- : mString(getEmptyString()) {
- setTo(o, len, begin);
-}
-
-String16::String16(const char16_t* o)
- : mString(allocFromUTF16(o, strlen16(o))) {}
-
-String16::String16(const char16_t* o, size_t len)
- : mString(allocFromUTF16(o, len)) {}
-
-String16::String16(const String8& o)
- : mString(allocFromUTF8(o.string(), o.size())) {}
-
-String16::String16(const char* o) : mString(allocFromUTF8(o, strlen(o))) {}
-
-String16::String16(const char* o, size_t len)
- : mString(allocFromUTF8(o, len)) {}
-
-String16::~String16() { release(); }
-
-size_t String16::size() const {
- if (isStaticString()) {
- return staticStringSize();
- } else {
- return SharedBuffer::sizeFromData(mString) / sizeof(char16_t) - 1;
- }
-}
-
-void String16::setTo(const String16& other) {
- release();
- mString = other.mString;
- acquire();
-}
-
-status_t String16::setTo(const String16& other, size_t len, size_t begin) {
- const size_t N = other.size();
- if (begin >= N) {
- release();
- mString = getEmptyString();
- return OK;
- }
- if ((begin + len) > N) len = N - begin;
- if (begin == 0 && len == N) {
- setTo(other);
- return OK;
- }
-
- if (&other == this) {
- LOG_ALWAYS_FATAL("Not implemented");
- }
-
- return setTo(other.string() + begin, len);
-}
-
-status_t String16::setTo(const char16_t* other) {
- return setTo(other, strlen16(other));
-}
-
-status_t String16::setTo(const char16_t* other, size_t len) {
- if (len >= SIZE_MAX / sizeof(char16_t)) {
- android_errorWriteLog(0x534e4554, "73826242");
- abort();
- }
-
- SharedBuffer* buf =
- static_cast<SharedBuffer*>(editResize((len + 1) * sizeof(char16_t)));
- if (buf) {
- char16_t* str = static_cast<char16_t*>(buf->data());
- memmove(str, other, len * sizeof(char16_t));
- str[len] = 0;
- mString = str;
- return OK;
- }
- return NO_MEMORY;
-}
-
-status_t String16::append(const String16& other) {
- const size_t myLen = size();
- const size_t otherLen = other.size();
- if (myLen == 0) {
- setTo(other);
- return OK;
- } else if (otherLen == 0) {
- return OK;
- }
-
- if (myLen >= SIZE_MAX / sizeof(char16_t) - otherLen) {
- android_errorWriteLog(0x534e4554, "73826242");
- abort();
- }
-
- SharedBuffer* buf = static_cast<SharedBuffer*>(
- editResize((myLen + otherLen + 1) * sizeof(char16_t)));
- if (buf) {
- char16_t* str = static_cast<char16_t*>(buf->data());
- memcpy(str + myLen, other, (otherLen + 1) * sizeof(char16_t));
- mString = str;
- return OK;
- }
- return NO_MEMORY;
-}
-
-status_t String16::append(const char16_t* chrs, size_t otherLen) {
- const size_t myLen = size();
- if (myLen == 0) {
- setTo(chrs, otherLen);
- return OK;
- } else if (otherLen == 0) {
- return OK;
- }
-
- if (myLen >= SIZE_MAX / sizeof(char16_t) - otherLen) {
- android_errorWriteLog(0x534e4554, "73826242");
- abort();
- }
-
- SharedBuffer* buf = static_cast<SharedBuffer*>(
- editResize((myLen + otherLen + 1) * sizeof(char16_t)));
- if (buf) {
- char16_t* str = static_cast<char16_t*>(buf->data());
- memcpy(str + myLen, chrs, otherLen * sizeof(char16_t));
- str[myLen + otherLen] = 0;
- mString = str;
- return OK;
- }
- return NO_MEMORY;
-}
-
-status_t String16::insert(size_t pos, const char16_t* chrs) {
- return insert(pos, chrs, strlen16(chrs));
-}
-
-status_t String16::insert(size_t pos, const char16_t* chrs, size_t len) {
- const size_t myLen = size();
- if (myLen == 0) {
- return setTo(chrs, len);
- return OK;
- } else if (len == 0) {
- return OK;
- }
-
- if (pos > myLen) pos = myLen;
-
-#if 0
- printf("Insert in to %s: pos=%d, len=%d, myLen=%d, chrs=%s\n",
- String8(*this).string(), pos,
- len, myLen, String8(chrs, len).string());
-#endif
-
- SharedBuffer* buf = static_cast<SharedBuffer*>(
- editResize((myLen + len + 1) * sizeof(char16_t)));
- if (buf) {
- char16_t* str = static_cast<char16_t*>(buf->data());
- if (pos < myLen) {
- memmove(str + pos + len, str + pos, (myLen - pos) * sizeof(char16_t));
- }
- memcpy(str + pos, chrs, len * sizeof(char16_t));
- str[myLen + len] = 0;
- mString = str;
-#if 0
- printf("Result (%d chrs): %s\n", size(), String8(*this).string());
-#endif
- return OK;
- }
- return NO_MEMORY;
-}
-
-ssize_t String16::findFirst(char16_t c) const {
- const char16_t* str = string();
- const char16_t* p = str;
- const char16_t* e = p + size();
- while (p < e) {
- if (*p == c) {
- return p - str;
- }
- p++;
- }
- return -1;
-}
-
-ssize_t String16::findLast(char16_t c) const {
- const char16_t* str = string();
- const char16_t* p = str;
- const char16_t* e = p + size();
- while (p < e) {
- e--;
- if (*e == c) {
- return e - str;
- }
- }
- return -1;
-}
-
-bool String16::startsWith(const String16& prefix) const {
- const size_t ps = prefix.size();
- if (ps > size()) return false;
- return strzcmp16(mString, ps, prefix.string(), ps) == 0;
-}
-
-bool String16::startsWith(const char16_t* prefix) const {
- const size_t ps = strlen16(prefix);
- if (ps > size()) return false;
- return strncmp16(mString, prefix, ps) == 0;
-}
-
-bool String16::contains(const char16_t* chrs) const {
- return strstr16(mString, chrs) != nullptr;
-}
-
-void* String16::edit() {
- SharedBuffer* buf;
- if (isStaticString()) {
- buf = static_cast<SharedBuffer*>(alloc((size() + 1) * sizeof(char16_t)));
- if (buf) {
- memcpy(buf->data(), mString, (size() + 1) * sizeof(char16_t));
- }
- } else {
- buf = SharedBuffer::bufferFromData(mString)->edit();
- buf->mClientMetadata = kIsSharedBufferAllocated;
- }
- return buf;
-}
-
-void* String16::editResize(size_t newSize) {
- SharedBuffer* buf;
- if (isStaticString()) {
- size_t copySize = (size() + 1) * sizeof(char16_t);
- if (newSize < copySize) {
- copySize = newSize;
- }
- buf = static_cast<SharedBuffer*>(alloc(newSize));
- if (buf) {
- memcpy(buf->data(), mString, copySize);
- }
- } else {
- buf = SharedBuffer::bufferFromData(mString)->editResize(newSize);
- buf->mClientMetadata = kIsSharedBufferAllocated;
- }
- return buf;
-}
-
-void String16::acquire() {
- if (!isStaticString()) {
- SharedBuffer::bufferFromData(mString)->acquire();
- }
-}
-
-void String16::release() {
- if (!isStaticString()) {
- SharedBuffer::bufferFromData(mString)->release();
- }
-}
-
-bool String16::isStaticString() const {
- // See String16.h for notes on the memory layout of String16::StaticData and
- // SharedBuffer.
- static_assert(
- sizeof(SharedBuffer) - offsetof(SharedBuffer, mClientMetadata) == 4);
- const uint32_t* p = reinterpret_cast<const uint32_t*>(mString);
- return (*(p - 1) & kIsSharedBufferAllocated) == 0;
-}
-
-size_t String16::staticStringSize() const {
- // See String16.h for notes on the memory layout of String16::StaticData and
- // SharedBuffer.
- static_assert(
- sizeof(SharedBuffer) - offsetof(SharedBuffer, mClientMetadata) == 4);
- const uint32_t* p = reinterpret_cast<const uint32_t*>(mString);
- return static_cast<size_t>(*(p - 1));
-}
-
-status_t String16::makeLower() {
- const size_t N = size();
- const char16_t* str = string();
- char16_t* edited = nullptr;
- for (size_t i = 0; i < N; i++) {
- const char16_t v = str[i];
- if (v >= 'A' && v <= 'Z') {
- if (!edited) {
- SharedBuffer* buf = static_cast<SharedBuffer*>(edit());
- if (!buf) {
- return NO_MEMORY;
- }
- edited = static_cast<char16_t*>(buf->data());
- mString = str = edited;
- }
- edited[i] = tolower(static_cast<char>(v));
- }
- }
- return OK;
-}
-
-status_t String16::replaceAll(char16_t replaceThis, char16_t withThis) {
- const size_t N = size();
- const char16_t* str = string();
- char16_t* edited = nullptr;
- for (size_t i = 0; i < N; i++) {
- if (str[i] == replaceThis) {
- if (!edited) {
- SharedBuffer* buf = static_cast<SharedBuffer*>(edit());
- if (!buf) {
- return NO_MEMORY;
- }
- edited = static_cast<char16_t*>(buf->data());
- mString = str = edited;
- }
- edited[i] = withThis;
- }
- }
- return OK;
-}
-
-status_t String16::remove(size_t len, size_t begin) {
- const size_t N = size();
- if (begin >= N) {
- release();
- mString = getEmptyString();
- return OK;
- }
- if (len > N || len > N - begin) len = N - begin;
- if (begin == 0 && len == N) {
- return OK;
- }
-
- if (begin > 0) {
- SharedBuffer* buf =
- static_cast<SharedBuffer*>(editResize((N + 1) * sizeof(char16_t)));
- if (!buf) {
- return NO_MEMORY;
- }
- char16_t* str = static_cast<char16_t*>(buf->data());
- memmove(str, str + begin, (N - begin + 1) * sizeof(char16_t));
- mString = str;
- }
- SharedBuffer* buf =
- static_cast<SharedBuffer*>(editResize((len + 1) * sizeof(char16_t)));
- if (buf) {
- char16_t* str = static_cast<char16_t*>(buf->data());
- str[len] = 0;
- mString = str;
- return OK;
- }
- return NO_MEMORY;
-}
-
-}; // namespace android
diff --git a/nativeruntime/cpp/libutils/String8.cpp b/nativeruntime/cpp/libutils/String8.cpp
deleted file mode 100644
index 140706451..000000000
--- a/nativeruntime/cpp/libutils/String8.cpp
+++ /dev/null
@@ -1,575 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/String8.cpp
-
-#define __STDC_LIMIT_MACROS
-#include <ctype.h>
-#include <log/log.h>
-#include <stdint.h>
-#include <utils/Compat.h>
-#include <utils/String16.h>
-#include <utils/String8.h>
-
-#include "SharedBuffer.h"
-
-/*
- * Functions outside android is below the namespace android, since they use
- * functions and constants in android namespace.
- */
-
-// ---------------------------------------------------------------------------
-
-namespace android {
-
-// Separator used by resource paths. This is not platform dependent contrary
-// to OS_PATH_SEPARATOR.
-#define RES_PATH_SEPARATOR '/'
-
-static inline char* getEmptyString() {
- static SharedBuffer* gEmptyStringBuf = [] {
- SharedBuffer* buf = SharedBuffer::alloc(1);
- char* str = static_cast<char*>(buf->data());
- *str = 0;
- return buf;
- }();
-
- gEmptyStringBuf->acquire();
- return static_cast<char*>(gEmptyStringBuf->data());
-}
-
-// ---------------------------------------------------------------------------
-
-static char* allocFromUTF8(const char* in, size_t len) {
- if (len > 0) {
- if (len == SIZE_MAX) {
- return nullptr;
- }
- SharedBuffer* buf = SharedBuffer::alloc(len + 1);
- ALOG_ASSERT(buf, "Unable to allocate shared buffer");
- if (buf) {
- char* str = static_cast<char*>(buf->data());
- memcpy(str, in, len);
- str[len] = 0;
- return str;
- }
- return nullptr;
- }
-
- return getEmptyString();
-}
-
-static char* allocFromUTF16(const char16_t* in, size_t len) {
- if (len == 0) return getEmptyString();
-
- // Allow for closing '\0'
- const ssize_t resultStrLen = utf16_to_utf8_length(in, len) + 1;
- if (resultStrLen < 1) {
- return getEmptyString();
- }
-
- SharedBuffer* buf = SharedBuffer::alloc(resultStrLen);
- ALOG_ASSERT(buf, "Unable to allocate shared buffer");
- if (!buf) {
- return getEmptyString();
- }
-
- char* resultStr = static_cast<char*>(buf->data());
- utf16_to_utf8(in, len, resultStr, resultStrLen);
- return resultStr;
-}
-
-static char* allocFromUTF32(const char32_t* in, size_t len) {
- if (len == 0) {
- return getEmptyString();
- }
-
- const ssize_t resultStrLen = utf32_to_utf8_length(in, len) + 1;
- if (resultStrLen < 1) {
- return getEmptyString();
- }
-
- SharedBuffer* buf = SharedBuffer::alloc(resultStrLen);
- ALOG_ASSERT(buf, "Unable to allocate shared buffer");
- if (!buf) {
- return getEmptyString();
- }
-
- char* resultStr = static_cast<char*>(buf->data());
- utf32_to_utf8(in, len, resultStr, resultStrLen);
-
- return resultStr;
-}
-
-// ---------------------------------------------------------------------------
-
-String8::String8() : mString(getEmptyString()) {}
-
-String8::String8(StaticLinkage) : mString(nullptr) {
- // this constructor is used when we can't rely on the static-initializers
- // having run. In this case we always allocate an empty string. It's less
- // efficient than using getEmptyString(), but we assume it's uncommon.
-
- char* data = static_cast<char*>(SharedBuffer::alloc(sizeof(char))->data());
- data[0] = 0;
- mString = data;
-}
-
-String8::String8(const String8& o) : mString(o.mString) {
- SharedBuffer::bufferFromData(mString)->acquire();
-}
-
-String8::String8(const char* o) : mString(allocFromUTF8(o, strlen(o))) {
- if (mString == nullptr) {
- mString = getEmptyString();
- }
-}
-
-String8::String8(const char* o, size_t len) : mString(allocFromUTF8(o, len)) {
- if (mString == nullptr) {
- mString = getEmptyString();
- }
-}
-
-String8::String8(const String16& o)
- : mString(allocFromUTF16(o.string(), o.size())) {}
-
-String8::String8(const char16_t* o) : mString(allocFromUTF16(o, strlen16(o))) {}
-
-String8::String8(const char16_t* o, size_t len)
- : mString(allocFromUTF16(o, len)) {}
-
-String8::String8(const char32_t* o) : mString(allocFromUTF32(o, strlen32(o))) {}
-
-String8::String8(const char32_t* o, size_t len)
- : mString(allocFromUTF32(o, len)) {}
-
-String8::~String8() {
- if (mString != nullptr) {
- SharedBuffer::bufferFromData(mString)->release();
- }
-}
-
-size_t String8::length() const {
- return SharedBuffer::sizeFromData(mString) - 1;
-}
-
-String8 String8::format(const char* fmt, ...) {
- va_list args;
- va_start(args, fmt);
-
- String8 result(formatV(fmt, args));
-
- va_end(args);
- return result;
-}
-
-String8 String8::formatV(const char* fmt, va_list args) {
- String8 result;
- result.appendFormatV(fmt, args);
- return result;
-}
-
-void String8::clear() {
- SharedBuffer::bufferFromData(mString)->release();
- mString = getEmptyString();
-}
-
-void String8::setTo(const String8& other) {
- SharedBuffer::bufferFromData(other.mString)->acquire();
- SharedBuffer::bufferFromData(mString)->release();
- mString = other.mString;
-}
-
-status_t String8::setTo(const char* other) {
- const char* newString = allocFromUTF8(other, strlen(other));
- SharedBuffer::bufferFromData(mString)->release();
- mString = newString;
- if (mString) return OK;
-
- mString = getEmptyString();
- return NO_MEMORY;
-}
-
-status_t String8::setTo(const char* other, size_t len) {
- const char* newString = allocFromUTF8(other, len);
- SharedBuffer::bufferFromData(mString)->release();
- mString = newString;
- if (mString) return OK;
-
- mString = getEmptyString();
- return NO_MEMORY;
-}
-
-status_t String8::setTo(const char16_t* other, size_t len) {
- const char* newString = allocFromUTF16(other, len);
- SharedBuffer::bufferFromData(mString)->release();
- mString = newString;
- if (mString) return OK;
-
- mString = getEmptyString();
- return NO_MEMORY;
-}
-
-status_t String8::setTo(const char32_t* other, size_t len) {
- const char* newString = allocFromUTF32(other, len);
- SharedBuffer::bufferFromData(mString)->release();
- mString = newString;
- if (mString) return OK;
-
- mString = getEmptyString();
- return NO_MEMORY;
-}
-
-status_t String8::append(const String8& other) {
- const size_t otherLen = other.bytes();
- if (bytes() == 0) {
- setTo(other);
- return OK;
- } else if (otherLen == 0) {
- return OK;
- }
-
- return real_append(other.string(), otherLen);
-}
-
-status_t String8::append(const char* other) {
- return append(other, strlen(other));
-}
-
-status_t String8::append(const char* other, size_t otherLen) {
- if (bytes() == 0) {
- return setTo(other, otherLen);
- } else if (otherLen == 0) {
- return OK;
- }
-
- return real_append(other, otherLen);
-}
-
-status_t String8::appendFormat(const char* fmt, ...) {
- va_list args;
- va_start(args, fmt);
-
- status_t result = appendFormatV(fmt, args);
-
- va_end(args);
- return result;
-}
-
-status_t String8::appendFormatV(const char* fmt, va_list args) {
- int n, result = OK;
- va_list tmp_args;
-
- /* args is undefined after vsnprintf.
- * So we need a copy here to avoid the
- * second vsnprintf access undefined args.
- */
- va_copy(tmp_args, args);
- n = vsnprintf(nullptr, 0, fmt, tmp_args);
- va_end(tmp_args);
-
- if (n != 0) {
- size_t oldLength = length();
- char* buf = lockBuffer(oldLength + n);
- if (buf) {
- vsnprintf(buf + oldLength, n + 1, fmt, args);
- } else {
- result = NO_MEMORY;
- }
- }
- return result;
-}
-
-status_t String8::real_append(const char* other, size_t otherLen) {
- const size_t myLen = bytes();
-
- SharedBuffer* buf =
- SharedBuffer::bufferFromData(mString)->editResize(myLen + otherLen + 1);
- if (buf) {
- char* str = static_cast<char*>(buf->data());
- mString = str;
- str += myLen;
- memcpy(str, other, otherLen);
- str[otherLen] = '\0';
- return OK;
- }
- return NO_MEMORY;
-}
-
-char* String8::lockBuffer(size_t size) {
- SharedBuffer* buf =
- SharedBuffer::bufferFromData(mString)->editResize(size + 1);
- if (buf) {
- char* str = static_cast<char*>(buf->data());
- mString = str;
- return str;
- }
- return nullptr;
-}
-
-void String8::unlockBuffer() { unlockBuffer(strlen(mString)); }
-
-status_t String8::unlockBuffer(size_t size) {
- if (size != this->size()) {
- SharedBuffer* buf =
- SharedBuffer::bufferFromData(mString)->editResize(size + 1);
- if (!buf) {
- return NO_MEMORY;
- }
-
- char* str = static_cast<char*>(buf->data());
- str[size] = 0;
- mString = str;
- }
-
- return OK;
-}
-
-ssize_t String8::find(const char* other, size_t start) const {
- size_t len = size();
- if (start >= len) {
- return -1;
- }
- const char* s = mString + start;
- const char* p = strstr(s, other);
- return p ? p - mString : -1;
-}
-
-bool String8::removeAll(const char* other) {
- ssize_t index = find(other);
- if (index < 0) return false;
-
- char* buf = lockBuffer(size());
- if (!buf) return false; // out of memory
-
- size_t skip = strlen(other);
- size_t len = size();
- size_t tail = index;
- while (size_t(index) < len) {
- ssize_t next = find(other, index + skip);
- if (next < 0) {
- next = len;
- }
-
- memmove(buf + tail, buf + index + skip, next - index - skip);
- tail += next - index - skip;
- index = next;
- }
- unlockBuffer(tail);
- return true;
-}
-
-void String8::toLower() { toLower(0, size()); }
-
-void String8::toLower(size_t start, size_t length) {
- const size_t len = size();
- if (start >= len) {
- return;
- }
- if (start + length > len) {
- length = len - start;
- }
- char* buf = lockBuffer(len);
- buf += start;
- while (length > 0) {
- *buf = tolower(*buf);
- buf++;
- length--;
- }
- unlockBuffer(len);
-}
-
-void String8::toUpper() { toUpper(0, size()); }
-
-void String8::toUpper(size_t start, size_t length) {
- const size_t len = size();
- if (start >= len) {
- return;
- }
- if (start + length > len) {
- length = len - start;
- }
- char* buf = lockBuffer(len);
- buf += start;
- while (length > 0) {
- *buf = toupper(*buf);
- buf++;
- length--;
- }
- unlockBuffer(len);
-}
-
-// ---------------------------------------------------------------------------
-// Path functions
-
-void String8::setPathName(const char* name) { setPathName(name, strlen(name)); }
-
-void String8::setPathName(const char* name, size_t len) {
- char* buf = lockBuffer(len);
-
- memcpy(buf, name, len);
-
- // remove trailing path separator, if present
- if (len > 0 && buf[len - 1] == OS_PATH_SEPARATOR) len--;
-
- buf[len] = '\0';
-
- unlockBuffer(len);
-}
-
-String8 String8::getPathLeaf() const {
- const char* cp;
- const char* const buf = mString;
-
- cp = strrchr(buf, OS_PATH_SEPARATOR);
- if (cp == nullptr)
- return String8(*this);
- else
- return String8(cp + 1);
-}
-
-String8 String8::getPathDir() const {
- const char* cp;
- const char* const str = mString;
-
- cp = strrchr(str, OS_PATH_SEPARATOR);
- if (cp == nullptr)
- return String8("");
- else
- return String8(str, cp - str);
-}
-
-String8 String8::walkPath(String8* outRemains) const {
- const char* cp;
- const char* const str = mString;
- const char* buf = str;
-
- cp = strchr(buf, OS_PATH_SEPARATOR);
- if (cp == buf) {
- // don't include a leading '/'.
- buf = buf + 1;
- cp = strchr(buf, OS_PATH_SEPARATOR);
- }
-
- if (cp == nullptr) {
- String8 res = buf != str ? String8(buf) : *this;
- if (outRemains) *outRemains = String8("");
- return res;
- }
-
- String8 res(buf, cp - buf);
- if (outRemains) *outRemains = String8(cp + 1);
- return res;
-}
-
-/*
- * Helper function for finding the start of an extension in a pathname.
- *
- * Returns a pointer inside mString, or NULL if no extension was found.
- */
-char* String8::find_extension() const {
- const char* lastSlash;
- const char* lastDot;
- const char* const str = mString;
-
- // only look at the filename
- lastSlash = strrchr(str, OS_PATH_SEPARATOR);
- if (lastSlash == nullptr)
- lastSlash = str;
- else
- lastSlash++;
-
- // find the last dot
- lastDot = strrchr(lastSlash, '.');
- if (lastDot == nullptr) return nullptr;
-
- // looks good, ship it
- return const_cast<char*>(lastDot);
-}
-
-String8 String8::getPathExtension() const {
- char* ext;
-
- ext = find_extension();
- if (ext != nullptr)
- return String8(ext);
- else
- return String8("");
-}
-
-String8 String8::getBasePath() const {
- char* ext;
- const char* const str = mString;
-
- ext = find_extension();
- if (ext == nullptr)
- return String8(*this);
- else
- return String8(str, ext - str);
-}
-
-String8& String8::appendPath(const char* name) {
- // TODO: The test below will fail for Win32 paths. Fix later or ignore.
- if (name[0] != OS_PATH_SEPARATOR) {
- if (*name == '\0') {
- // nothing to do
- return *this;
- }
-
- size_t len = length();
- if (len == 0) {
- // no existing filename, just use the new one
- setPathName(name);
- return *this;
- }
-
- // make room for oldPath + '/' + newPath
- int newlen = strlen(name);
-
- char* buf = lockBuffer(len + 1 + newlen);
-
- // insert a '/' if needed
- if (buf[len - 1] != OS_PATH_SEPARATOR) buf[len++] = OS_PATH_SEPARATOR;
-
- memcpy(buf + len, name, newlen + 1);
- len += newlen;
-
- unlockBuffer(len);
-
- return *this;
- } else {
- setPathName(name);
- return *this;
- }
-}
-
-String8& String8::convertToResPath() {
-#if OS_PATH_SEPARATOR != RES_PATH_SEPARATOR
- size_t len = length();
- if (len > 0) {
- char* buf = lockBuffer(len);
- for (char* end = buf + len; buf < end; ++buf) {
- if (*buf == OS_PATH_SEPARATOR) *buf = RES_PATH_SEPARATOR;
- }
- unlockBuffer(len);
- }
-#endif
- return *this;
-}
-
-}; // namespace android
diff --git a/nativeruntime/cpp/libutils/Unicode.cpp b/nativeruntime/cpp/libutils/Unicode.cpp
deleted file mode 100644
index 64c769702..000000000
--- a/nativeruntime/cpp/libutils/Unicode.cpp
+++ /dev/null
@@ -1,561 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/Unicode.cpp
-
-#define LOG_TAG "unicode"
-
-#include <android-base/macros.h>
-#include <limits.h>
-#include <log/log.h>
-#include <utils/Unicode.h>
-
-#if defined(_WIN32)
-#undef nhtol
-#undef htonl
-#undef nhtos
-#undef htons
-
-#define ntohl(x) \
- (((x) << 24) | (((x) >> 24) & 255) | (((x) << 8) & 0xff0000) | \
- (((x) >> 8) & 0xff00))
-#define htonl(x) ntohl(x)
-#define ntohs(x) ((((x) << 8) & 0xff00) | (((x) >> 8) & 255))
-#define htons(x) ntohs(x)
-#else
-#include <netinet/in.h>
-#endif
-
-extern "C" {
-
-static const char32_t kByteMask = 0x000000BF;
-static const char32_t kByteMark = 0x00000080;
-
-// Surrogates aren't valid for UTF-32 characters, so define some
-// constants that will let us screen them out.
-static const char32_t kUnicodeSurrogateHighStart = 0x0000D800;
-// Unused, here for completeness:
-// static const char32_t kUnicodeSurrogateHighEnd = 0x0000DBFF;
-// static const char32_t kUnicodeSurrogateLowStart = 0x0000DC00;
-static const char32_t kUnicodeSurrogateLowEnd = 0x0000DFFF;
-static const char32_t kUnicodeSurrogateStart = kUnicodeSurrogateHighStart;
-static const char32_t kUnicodeSurrogateEnd = kUnicodeSurrogateLowEnd;
-static const char32_t kUnicodeMaxCodepoint = 0x0010FFFF;
-
-// Mask used to set appropriate bits in first byte of UTF-8 sequence,
-// indexed by number of bytes in the sequence.
-// 0xxxxxxx
-// -> (00-7f) 7bit. Bit mask for the first byte is 0x00000000
-// 110yyyyx 10xxxxxx
-// -> (c0-df)(80-bf) 11bit. Bit mask is 0x000000C0
-// 1110yyyy 10yxxxxx 10xxxxxx
-// -> (e0-ef)(80-bf)(80-bf) 16bit. Bit mask is 0x000000E0
-// 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx
-// -> (f0-f7)(80-bf)(80-bf)(80-bf) 21bit. Bit mask is 0x000000F0
-static const char32_t kFirstByteMark[] = {0x00000000, 0x00000000, 0x000000C0,
- 0x000000E0, 0x000000F0};
-
-// --------------------------------------------------------------------------
-// UTF-32
-// --------------------------------------------------------------------------
-
-/**
- * Return number of UTF-8 bytes required for the character. If the character
- * is invalid, return size of 0.
- */
-static inline size_t utf32_codepoint_utf8_length(char32_t srcChar) {
- // Figure out how many bytes the result will require.
- if (srcChar < 0x00000080) {
- return 1;
- } else if (srcChar < 0x00000800) {
- return 2;
- } else if (srcChar < 0x00010000) {
- if ((srcChar < kUnicodeSurrogateStart) ||
- (srcChar > kUnicodeSurrogateEnd)) {
- return 3;
- } else {
- // Surrogates are invalid UTF-32 characters.
- return 0;
- }
- }
- // Max code point for Unicode is 0x0010FFFF.
- else if (srcChar <= kUnicodeMaxCodepoint) {
- return 4;
- } else {
- // Invalid UTF-32 character.
- return 0;
- }
-}
-
-// Write out the source character to <dstP>.
-
-static inline void utf32_codepoint_to_utf8(uint8_t *dstP, char32_t srcChar,
- size_t bytes) {
- dstP += bytes;
- switch (bytes) { /* note: everything falls through. */
- case 4:
- *--dstP = (uint8_t)((srcChar | kByteMark) & kByteMask);
- srcChar >>= 6;
- FALLTHROUGH_INTENDED;
- case 3:
- *--dstP = (uint8_t)((srcChar | kByteMark) & kByteMask);
- srcChar >>= 6;
- FALLTHROUGH_INTENDED;
- case 2:
- *--dstP = (uint8_t)((srcChar | kByteMark) & kByteMask);
- srcChar >>= 6;
- FALLTHROUGH_INTENDED;
- case 1:
- *--dstP = (uint8_t)(srcChar | kFirstByteMark[bytes]);
- }
-}
-
-size_t strlen32(const char32_t *s) {
- const char32_t *ss = s;
- while (*ss) ss++;
- return ss - s;
-}
-
-size_t strnlen32(const char32_t *s, size_t maxlen) {
- const char32_t *ss = s;
- while ((maxlen > 0) && *ss) {
- ss++;
- maxlen--;
- }
- return ss - s;
-}
-
-static inline int32_t utf32_at_internal(const char *cur, size_t *num_read) {
- const char first_char = *cur;
- if ((first_char & 0x80) == 0) { // ASCII
- *num_read = 1;
- return *cur;
- }
- cur++;
- char32_t mask, to_ignore_mask;
- size_t num_to_read = 0;
- char32_t utf32 = first_char;
- for (num_to_read = 1, mask = 0x40, to_ignore_mask = 0xFFFFFF80;
- (first_char & mask); num_to_read++, to_ignore_mask |= mask, mask >>= 1) {
- // 0x3F == 00111111
- utf32 = (utf32 << 6) + (*cur++ & 0x3F);
- }
- to_ignore_mask |= mask;
- utf32 &= ~(to_ignore_mask << (6 * (num_to_read - 1)));
-
- *num_read = num_to_read;
- return static_cast<int32_t>(utf32);
-}
-
-int32_t utf32_from_utf8_at(const char *src, size_t src_len, size_t index,
- size_t *next_index) {
- if (index >= src_len) {
- return -1;
- }
- size_t dummy_index;
- if (next_index == nullptr) {
- next_index = &dummy_index;
- }
- size_t num_read;
- int32_t ret = utf32_at_internal(src + index, &num_read);
- if (ret >= 0) {
- *next_index = index + num_read;
- }
-
- return ret;
-}
-
-ssize_t utf32_to_utf8_length(const char32_t *src, size_t src_len) {
- if (src == nullptr || src_len == 0) {
- return -1;
- }
-
- size_t ret = 0;
- const char32_t *end = src + src_len;
- while (src < end) {
- size_t char_len = utf32_codepoint_utf8_length(*src++);
- if (SSIZE_MAX - char_len < ret) {
- // If this happens, we would overflow the ssize_t type when
- // returning from this function, so we cannot express how
- // long this string is in an ssize_t.
- android_errorWriteLog(0x534e4554, "37723026");
- return -1;
- }
- ret += char_len;
- }
- return ret;
-}
-
-void utf32_to_utf8(const char32_t *src, size_t src_len, char *dst,
- size_t dst_len) {
- if (src == nullptr || src_len == 0 || dst == nullptr) {
- return;
- }
-
- const char32_t *cur_utf32 = src;
- const char32_t *end_utf32 = src + src_len;
- char *cur = dst;
- while (cur_utf32 < end_utf32) {
- size_t len = utf32_codepoint_utf8_length(*cur_utf32);
- LOG_ALWAYS_FATAL_IF(dst_len < len, "%zu < %zu", dst_len, len);
- utf32_codepoint_to_utf8((uint8_t *)cur, *cur_utf32++, len);
- cur += len;
- dst_len -= len;
- }
- LOG_ALWAYS_FATAL_IF(dst_len < 1, "dst_len < 1: %zu < 1", dst_len);
- *cur = '\0';
-}
-
-// --------------------------------------------------------------------------
-// UTF-16
-// --------------------------------------------------------------------------
-
-int strcmp16(const char16_t *s1, const char16_t *s2) {
- char16_t ch;
- int d = 0;
-
- while (true) {
- d = (int)(ch = *s1++) - (int)*s2++;
- if (d || !ch) break;
- }
-
- return d;
-}
-
-int strncmp16(const char16_t *s1, const char16_t *s2, size_t n) {
- char16_t ch;
- int d = 0;
-
- if (n == 0) {
- return 0;
- }
-
- do {
- d = (int)(ch = *s1++) - (int)*s2++;
- if (d || !ch) {
- break;
- }
- } while (--n);
-
- return d;
-}
-
-char16_t *strcpy16(char16_t *dst, const char16_t *src) {
- char16_t *q = dst;
- const char16_t *p = src;
- char16_t ch;
-
- do {
- *q++ = ch = *p++;
- } while (ch);
-
- return dst;
-}
-
-size_t strlen16(const char16_t *s) {
- const char16_t *ss = s;
- while (*ss) ss++;
- return ss - s;
-}
-
-size_t strnlen16(const char16_t *s, size_t maxlen) {
- const char16_t *ss = s;
-
- /* Important: the maxlen test must precede the reference through ss;
- since the byte beyond the maximum may segfault */
- while ((maxlen > 0) && *ss) {
- ss++;
- maxlen--;
- }
- return ss - s;
-}
-
-char16_t *strstr16(const char16_t *src, const char16_t *target) {
- const char16_t needle = *target;
- if (needle == '\0') return (char16_t *)src;
-
- const size_t target_len = strlen16(++target);
- do {
- do {
- if (*src == '\0') {
- return nullptr;
- }
- } while (*src++ != needle);
- } while (strncmp16(src, target, target_len) != 0);
- src--;
-
- return (char16_t *)src;
-}
-
-int strzcmp16(const char16_t *s1, size_t n1, const char16_t *s2, size_t n2) {
- const char16_t *e1 = s1 + n1;
- const char16_t *e2 = s2 + n2;
-
- while (s1 < e1 && s2 < e2) {
- const int d = (int)*s1++ - (int)*s2++;
- if (d) {
- return d;
- }
- }
-
- return n1 < n2 ? (0 - (int)*s2) : (n1 > n2 ? ((int)*s1 - 0) : 0);
-}
-
-void utf16_to_utf8(const char16_t *src, size_t src_len, char *dst,
- size_t dst_len) {
- if (src == nullptr || src_len == 0 || dst == nullptr) {
- return;
- }
-
- const char16_t *cur_utf16 = src;
- const char16_t *const end_utf16 = src + src_len;
- char *cur = dst;
- while (cur_utf16 < end_utf16) {
- char32_t utf32;
- // surrogate pairs
- if ((*cur_utf16 & 0xFC00) == 0xD800 && (cur_utf16 + 1) < end_utf16 &&
- (*(cur_utf16 + 1) & 0xFC00) == 0xDC00) {
- utf32 = (*cur_utf16++ - 0xD800) << 10;
- utf32 |= *cur_utf16++ - 0xDC00;
- utf32 += 0x10000;
- } else {
- utf32 = (char32_t)*cur_utf16++;
- }
- const size_t len = utf32_codepoint_utf8_length(utf32);
- LOG_ALWAYS_FATAL_IF(dst_len < len, "%zu < %zu", dst_len, len);
- utf32_codepoint_to_utf8((uint8_t *)cur, utf32, len);
- cur += len;
- dst_len -= len;
- }
- LOG_ALWAYS_FATAL_IF(dst_len < 1, "%zu < 1", dst_len);
- *cur = '\0';
-}
-
-// --------------------------------------------------------------------------
-// UTF-8
-// --------------------------------------------------------------------------
-
-ssize_t utf8_length(const char *src) {
- const char *cur = src;
- size_t ret = 0;
- while (*cur != '\0') {
- const char first_char = *cur++;
- if ((first_char & 0x80) == 0) { // ASCII
- ret += 1;
- continue;
- }
- // (UTF-8's character must not be like 10xxxxxx,
- // but 110xxxxx, 1110xxxx, ... or 1111110x)
- if ((first_char & 0x40) == 0) {
- return -1;
- }
-
- int32_t mask, to_ignore_mask;
- size_t num_to_read = 0;
- char32_t utf32 = 0;
- for (num_to_read = 1, mask = 0x40, to_ignore_mask = 0x80;
- num_to_read < 5 && (first_char & mask);
- num_to_read++, to_ignore_mask |= mask, mask >>= 1) {
- if ((*cur & 0xC0) != 0x80) { // must be 10xxxxxx
- return -1;
- }
- // 0x3F == 00111111
- utf32 = (utf32 << 6) + (*cur++ & 0x3F);
- }
- // "first_char" must be (110xxxxx - 11110xxx)
- if (num_to_read == 5) {
- return -1;
- }
- to_ignore_mask |= mask;
- utf32 |= ((~to_ignore_mask) & first_char) << (6 * (num_to_read - 1));
- if (utf32 > kUnicodeMaxCodepoint) {
- return -1;
- }
-
- ret += num_to_read;
- }
- return ret;
-}
-
-ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len) {
- if (src == nullptr || src_len == 0) {
- return -1;
- }
-
- size_t ret = 0;
- const char16_t *const end = src + src_len;
- while (src < end) {
- size_t char_len;
- if ((*src & 0xFC00) == 0xD800 && (src + 1) < end &&
- (*(src + 1) & 0xFC00) == 0xDC00) {
- // surrogate pairs are always 4 bytes.
- char_len = 4;
- src += 2;
- } else {
- char_len = utf32_codepoint_utf8_length((char32_t)*src++);
- }
- if (SSIZE_MAX - char_len < ret) {
- // If this happens, we would overflow the ssize_t type when
- // returning from this function, so we cannot express how
- // long this string is in an ssize_t.
- android_errorWriteLog(0x534e4554, "37723026");
- return -1;
- }
- ret += char_len;
- }
- return ret;
-}
-
-/**
- * Returns 1-4 based on the number of leading bits.
- *
- * 1111 -> 4
- * 1110 -> 3
- * 110x -> 2
- * 10xx -> 1
- * 0xxx -> 1
- */
-static inline size_t utf8_codepoint_len(uint8_t ch) {
- return ((0xe5000000 >> ((ch >> 3) & 0x1e)) & 3) + 1;
-}
-
-static inline void utf8_shift_and_mask(uint32_t *codePoint,
- const uint8_t byte) {
- *codePoint <<= 6;
- *codePoint |= 0x3F & byte;
-}
-
-static inline uint32_t utf8_to_utf32_codepoint(const uint8_t *src,
- size_t length) {
- uint32_t unicode;
-
- switch (length) {
- case 1:
- return src[0];
- case 2:
- unicode = src[0] & 0x1f;
- utf8_shift_and_mask(&unicode, src[1]);
- return unicode;
- case 3:
- unicode = src[0] & 0x0f;
- utf8_shift_and_mask(&unicode, src[1]);
- utf8_shift_and_mask(&unicode, src[2]);
- return unicode;
- case 4:
- unicode = src[0] & 0x07;
- utf8_shift_and_mask(&unicode, src[1]);
- utf8_shift_and_mask(&unicode, src[2]);
- utf8_shift_and_mask(&unicode, src[3]);
- return unicode;
- default:
- return 0xffff;
- }
-
- // printf("Char at %p: len=%d, utf-16=%p\n", src, length, (void*)result);
-}
-
-ssize_t utf8_to_utf16_length(const uint8_t *u8str, size_t u8len,
- bool overreadIsFatal) {
- const uint8_t *const u8end = u8str + u8len;
- const uint8_t *u8cur = u8str;
-
- /* Validate that the UTF-8 is the correct len */
- size_t u16measuredLen = 0;
- while (u8cur < u8end) {
- u16measuredLen++;
- int u8charLen = utf8_codepoint_len(*u8cur);
- // Malformed utf8, some characters are beyond the end.
- // Cases:
- // If u8charLen == 1, this becomes u8cur >= u8end, which cannot happen as
- // u8cur < u8end, then this condition fail and we continue, as expected. If
- // u8charLen == 2, this becomes u8cur + 1 >= u8end, which fails only if
- // u8cur == u8end - 1, that is, there was only one remaining character to
- // read but we need 2 of them. This condition holds and we return -1, as
- // expected.
- if (u8cur + u8charLen - 1 >= u8end) {
- if (overreadIsFatal) {
- LOG_ALWAYS_FATAL("Attempt to overread computing length of utf8 string");
- } else {
- return -1;
- }
- }
- uint32_t codepoint = utf8_to_utf32_codepoint(u8cur, u8charLen);
- if (codepoint > 0xFFFF)
- u16measuredLen++; // this will be a surrogate pair in utf16
- u8cur += u8charLen;
- }
-
- /**
- * Make sure that we ended where we thought we would and the output UTF-16
- * will be exactly how long we were told it would be.
- */
- if (u8cur != u8end) {
- return -1;
- }
-
- return u16measuredLen;
-}
-
-char16_t *utf8_to_utf16(const uint8_t *u8str, size_t u8len, char16_t *u16str,
- size_t u16len) {
- // A value > SSIZE_MAX is probably a negative value returned as an error and
- // casted.
- LOG_ALWAYS_FATAL_IF(u16len == 0 || u16len > SSIZE_MAX, "u16len is %zu",
- u16len);
- char16_t *end =
- utf8_to_utf16_no_null_terminator(u8str, u8len, u16str, u16len - 1);
- *end = 0;
- return end;
-}
-
-char16_t *utf8_to_utf16_no_null_terminator(const uint8_t *src, size_t srcLen,
- char16_t *dst, size_t dstLen) {
- if (dstLen == 0) {
- return dst;
- }
- // A value > SSIZE_MAX is probably a negative value returned as an error and
- // casted.
- LOG_ALWAYS_FATAL_IF(dstLen > SSIZE_MAX, "dstLen is %zu", dstLen);
- const uint8_t *const u8end = src + srcLen;
- const uint8_t *u8cur = src;
- const char16_t *const u16end = dst + dstLen;
- char16_t *u16cur = dst;
-
- while (u8cur < u8end && u16cur < u16end) {
- size_t u8len = utf8_codepoint_len(*u8cur);
- uint32_t codepoint = utf8_to_utf32_codepoint(u8cur, u8len);
-
- // Convert the UTF32 codepoint to one or more UTF16 codepoints
- if (codepoint <= 0xFFFF) {
- // Single UTF16 character
- *u16cur++ = (char16_t)codepoint;
- } else {
- // Multiple UTF16 characters with surrogates
- codepoint = codepoint - 0x10000;
- *u16cur++ = (char16_t)((codepoint >> 10) + 0xD800);
- if (u16cur >= u16end) {
- // Ooops... not enough room for this surrogate pair.
- return u16cur - 1;
- }
- *u16cur++ = (char16_t)((codepoint & 0x3FF) + 0xDC00);
- }
-
- u8cur += u8len;
- }
- return u16cur;
-}
-}
diff --git a/nativeruntime/cpp/libutils/include/utils/Compat.h b/nativeruntime/cpp/libutils/include/utils/Compat.h
deleted file mode 100644
index 2846441d3..000000000
--- a/nativeruntime/cpp/libutils/include/utils/Compat.h
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/include/utils/Compat.h
-
-#ifndef __LIB_UTILS_COMPAT_H
-#define __LIB_UTILS_COMPAT_H
-
-#include <unistd.h>
-
-#if !defined(__MINGW32__)
-#include <sys/mman.h>
-#endif
-
-#if defined(__APPLE__)
-
-/* Mac OS has always had a 64-bit off_t, so it doesn't have off64_t. */
-static_assert(sizeof(off_t) >= 8,
- "This code requires that Mac OS have at least a 64-bit off_t.");
-typedef off_t off64_t;
-
-static inline void* mmap64(void* addr, size_t length, int prot, int flags,
- int fd, off64_t offset) {
- return mmap(addr, length, prot, flags, fd, offset);
-}
-
-static inline off64_t lseek64(int fd, off64_t offset, int whence) {
- return lseek(fd, offset, whence);
-}
-
-static inline ssize_t pread64(int fd, void* buf, size_t nbytes,
- off64_t offset) {
- return pread(fd, buf, nbytes, offset);
-}
-
-static inline ssize_t pwrite64(int fd, const void* buf, size_t nbytes,
- off64_t offset) {
- return pwrite(fd, buf, nbytes, offset);
-}
-
-static inline int ftruncate64(int fd, off64_t length) {
- return ftruncate(fd, length);
-}
-
-#endif /* __APPLE__ */
-
-#if defined(_WIN32)
-#define O_CLOEXEC O_NOINHERIT
-#define O_NOFOLLOW 0
-#define DEFFILEMODE 0666
-#endif /* _WIN32 */
-
-#define ZD "%zd"
-#define ZD_TYPE ssize_t
-
-/*
- * Needed for cases where something should be constexpr if possible, but not
- * being constexpr is fine if in pre-C++11 code (such as a const static float
- * member variable).
- */
-#if __cplusplus >= 201103L
-#define CONSTEXPR constexpr
-#else
-#define CONSTEXPR
-#endif
-
-/*
- * TEMP_FAILURE_RETRY is defined by some, but not all, versions of
- * <unistd.h>. (Alas, it is not as standard as we'd hoped!) So, if it's
- * not already defined, then define it here.
- */
-#ifndef TEMP_FAILURE_RETRY
-/* Used to retry syscalls that can return EINTR. */
-#define TEMP_FAILURE_RETRY(exp) \
- ({ \
- typeof(exp) _rc; \
- do { \
- _rc = (exp); \
- } while (_rc == -1 && errno == EINTR); \
- _rc; \
- })
-#endif
-
-#if defined(_WIN32)
-#define OS_PATH_SEPARATOR '\\'
-#else
-#define OS_PATH_SEPARATOR '/'
-#endif
-
-#endif /* __LIB_UTILS_COMPAT_H */
diff --git a/nativeruntime/cpp/libutils/include/utils/Errors.h b/nativeruntime/cpp/libutils/include/utils/Errors.h
deleted file mode 100644
index 673f2ccbc..000000000
--- a/nativeruntime/cpp/libutils/include/utils/Errors.h
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/include/utils/Errors.h
-
-#ifndef ANDROID_ERRORS_H
-#define ANDROID_ERRORS_H
-
-#include <errno.h>
-#include <stdint.h>
-#include <sys/types.h>
-
-#include <string>
-
-namespace android {
-
-/**
- * The type used to return success/failure from frameworks APIs.
- * See the anonymous enum below for valid values.
- */
-typedef int32_t status_t;
-
-/*
- * Error codes.
- * All error codes are negative values.
- */
-
-// Win32 #defines NO_ERROR as well. It has the same value, so there's no
-// real conflict, though it's a bit awkward.
-#ifdef _WIN32
-#undef NO_ERROR
-#endif
-
-enum {
- OK = 0, // Preferred constant for checking success.
- NO_ERROR = OK, // Deprecated synonym for `OK`. Prefer `OK` because it doesn't
- // conflict with Windows.
-
- UNKNOWN_ERROR = (-2147483647 - 1), // INT32_MIN value
-
- NO_MEMORY = -ENOMEM,
- INVALID_OPERATION = -ENOSYS,
- BAD_VALUE = -EINVAL,
- BAD_TYPE = (UNKNOWN_ERROR + 1),
- NAME_NOT_FOUND = -ENOENT,
- PERMISSION_DENIED = -EPERM,
- NO_INIT = -ENODEV,
- ALREADY_EXISTS = -EEXIST,
- DEAD_OBJECT = -EPIPE,
- FAILED_TRANSACTION = (UNKNOWN_ERROR + 2),
-#if !defined(_WIN32)
- BAD_INDEX = -EOVERFLOW,
- NOT_ENOUGH_DATA = -ENODATA,
- WOULD_BLOCK = -EWOULDBLOCK,
- TIMED_OUT = -ETIMEDOUT,
- UNKNOWN_TRANSACTION = -EBADMSG,
-#else
- BAD_INDEX = -E2BIG,
- NOT_ENOUGH_DATA = (UNKNOWN_ERROR + 3),
- WOULD_BLOCK = (UNKNOWN_ERROR + 4),
- TIMED_OUT = (UNKNOWN_ERROR + 5),
- UNKNOWN_TRANSACTION = (UNKNOWN_ERROR + 6),
-#endif
- FDS_NOT_ALLOWED = (UNKNOWN_ERROR + 7),
- UNEXPECTED_NULL = (UNKNOWN_ERROR + 8),
-};
-
-// Human readable name of error
-std::string statusToString(status_t status);
-
-// Restore define; enumeration is in "android" namespace, so the value defined
-// there won't work for Win32 code in a different namespace.
-#ifdef _WIN32
-#define NO_ERROR 0L
-#endif
-
-} // namespace android
-
-#endif // ANDROID_ERRORS_H
diff --git a/nativeruntime/cpp/libutils/include/utils/String16.h b/nativeruntime/cpp/libutils/include/utils/String16.h
deleted file mode 100644
index 6794b3274..000000000
--- a/nativeruntime/cpp/libutils/include/utils/String16.h
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/include/utils/String16.h
-
-#ifndef ANDROID_STRING16_H
-#define ANDROID_STRING16_H
-
-#include <utils/Errors.h>
-#include <utils/String8.h>
-#include <utils/TypeHelpers.h>
-
-#include <iostream>
-#include <string>
-
-// ---------------------------------------------------------------------------
-
-namespace android {
-
-// ---------------------------------------------------------------------------
-
-template <size_t N>
-class StaticString16;
-
-// DO NOT USE: please use std::u16string
-
-//! This is a string holding UTF-16 characters.
-class String16 {
- public:
- /*
- * Use String16(StaticLinkage) if you're statically linking against
- * libutils and declaring an empty static String16, e.g.:
- *
- * static String16 sAStaticEmptyString(String16::kEmptyString);
- * static String16 sAnotherStaticEmptyString(sAStaticEmptyString);
- */
- enum StaticLinkage { kEmptyString };
-
- String16();
- explicit String16(StaticLinkage);
- String16(const String16& o);
- String16(const String16& o, size_t len, size_t begin = 0);
- explicit String16(const char16_t* o);
- explicit String16(const char16_t* o, size_t len);
- explicit String16(const String8& o);
- explicit String16(const char* o);
- explicit String16(const char* o, size_t len);
-
- ~String16();
-
- inline const char16_t* string() const;
-
- private:
- static inline std::string std_string(const String16& str);
-
- public:
- size_t size() const;
- void setTo(const String16& other);
- status_t setTo(const char16_t* other);
- status_t setTo(const char16_t* other, size_t len);
- status_t setTo(const String16& other, size_t len, size_t begin = 0);
-
- status_t append(const String16& other);
- status_t append(const char16_t* chrs, size_t len);
-
- inline String16& operator=(const String16& other);
-
- inline String16& operator+=(const String16& other);
- inline String16 operator+(const String16& other) const;
-
- status_t insert(size_t pos, const char16_t* chrs);
- status_t insert(size_t pos, const char16_t* chrs, size_t len);
-
- ssize_t findFirst(char16_t c) const;
- ssize_t findLast(char16_t c) const;
-
- bool startsWith(const String16& prefix) const;
- bool startsWith(const char16_t* prefix) const;
-
- bool contains(const char16_t* chrs) const;
-
- status_t makeLower();
-
- status_t replaceAll(char16_t replaceThis, char16_t withThis);
-
- status_t remove(size_t len, size_t begin = 0);
-
- inline int compare(const String16& other) const;
-
- inline bool operator<(const String16& other) const;
- inline bool operator<=(const String16& other) const;
- inline bool operator==(const String16& other) const;
- inline bool operator!=(const String16& other) const;
- inline bool operator>=(const String16& other) const;
- inline bool operator>(const String16& other) const;
-
- inline bool operator<(const char16_t* other) const;
- inline bool operator<=(const char16_t* other) const;
- inline bool operator==(const char16_t* other) const;
- inline bool operator!=(const char16_t* other) const;
- inline bool operator>=(const char16_t* other) const;
- inline bool operator>(const char16_t* other) const;
-
- inline operator const char16_t*() const;
-
- // Static and non-static String16 behave the same for the users, so
- // this method isn't of much use for the users. It is public for testing.
- bool isStaticString() const;
-
- private:
- /*
- * A flag indicating the type of underlying buffer.
- */
- static constexpr uint32_t kIsSharedBufferAllocated = 0x80000000;
-
- /*
- * alloc() returns void* so that SharedBuffer class is not exposed.
- */
- static void* alloc(size_t size);
- static char16_t* allocFromUTF8(const char* u8str, size_t u8len);
- static char16_t* allocFromUTF16(const char16_t* u16str, size_t u16len);
-
- /*
- * edit() and editResize() return void* so that SharedBuffer class
- * is not exposed.
- */
- void* edit();
- void* editResize(size_t newSize);
-
- void acquire();
- void release();
-
- size_t staticStringSize() const;
-
- const char16_t* mString;
-
- protected:
- /*
- * Data structure used to allocate static storage for static String16.
- *
- * Note that this data structure and SharedBuffer are used interchangeably
- * as the underlying data structure for a String16. Therefore, the layout
- * of this data structure must match the part in SharedBuffer that is
- * visible to String16.
- */
- template <size_t N>
- struct StaticData {
- // The high bit of 'size' is used as a flag.
- static_assert(N - 1 < kIsSharedBufferAllocated, "StaticString16 too long!");
- constexpr StaticData() : size(N - 1), data{0} {}
- const uint32_t size;
- char16_t data[N];
-
- constexpr StaticData(const StaticData<N>&) = default;
- };
-
- /*
- * Helper function for constructing a StaticData object.
- */
- template <size_t N>
- static constexpr const StaticData<N> makeStaticData(const char16_t (&s)[N]) {
- StaticData<N> r;
- // The 'size' field is at the same location where mClientMetadata would
- // be for a SharedBuffer. We do NOT set kIsSharedBufferAllocated flag
- // here.
- for (size_t i = 0; i < N - 1; ++i) r.data[i] = s[i];
- return r;
- }
-
- template <size_t N>
- explicit constexpr String16(const StaticData<N>& s) : mString(s.data) {}
-
- public:
- template <size_t N>
- explicit constexpr String16(const StaticString16<N>& s)
- : mString(s.mString) {}
-};
-
-// String16 can be trivially moved using memcpy() because moving does not
-// require any change to the underlying SharedBuffer contents or reference
-// count.
-ANDROID_TRIVIAL_MOVE_TRAIT(String16)
-
-static inline std::ostream& operator<<(std::ostream& os, const String16& str) {
- os << String8(str).c_str();
- return os;
-}
-
-// ---------------------------------------------------------------------------
-
-/*
- * A StaticString16 object is a specialized String16 object. Instead of holding
- * the string data in a ref counted SharedBuffer object, it holds data in a
- * buffer within StaticString16 itself. Note that this buffer is NOT ref
- * counted and is assumed to be available for as long as there is at least a
- * String16 object using it. Therefore, one must be extra careful to NEVER
- * assign a StaticString16 to a String16 that outlives the StaticString16
- * object.
- *
- * THE SAFEST APPROACH IS TO USE StaticString16 ONLY AS GLOBAL VARIABLES.
- *
- * A StaticString16 SHOULD NEVER APPEAR IN APIs. USE String16 INSTEAD.
- */
-template <size_t N>
-class StaticString16 : public String16 {
- public:
- constexpr StaticString16(const char16_t (&s)[N])
- : String16(mData), mData(makeStaticData(s)) {}
-
- constexpr StaticString16(const StaticString16<N>& other)
- : String16(mData), mData(other.mData) {}
-
- constexpr StaticString16(const StaticString16<N>&&) = delete;
-
- // There is no reason why one would want to 'new' a StaticString16. Delete
- // it to discourage misuse.
- static void* operator new(std::size_t) = delete;
-
- private:
- const StaticData<N> mData;
-};
-
-template <typename F>
-StaticString16(const F&) -> StaticString16<sizeof(F) / sizeof(char16_t)>;
-
-// ---------------------------------------------------------------------------
-// No user servicable parts below.
-
-inline int compare_type(const String16& lhs, const String16& rhs) {
- return lhs.compare(rhs);
-}
-
-inline int strictly_order_type(const String16& lhs, const String16& rhs) {
- return compare_type(lhs, rhs) < 0;
-}
-
-inline const char16_t* String16::string() const { return mString; }
-
-inline std::string String16::std_string(const String16& str) {
- return std::string(String8(str).string());
-}
-
-inline String16& String16::operator=(const String16& other) {
- setTo(other);
- return *this;
-}
-
-inline String16& String16::operator+=(const String16& other) {
- append(other);
- return *this;
-}
-
-inline String16 String16::operator+(const String16& other) const {
- String16 tmp(*this);
- tmp += other;
- return tmp;
-}
-
-inline int String16::compare(const String16& other) const {
- return strzcmp16(mString, size(), other.mString, other.size());
-}
-
-inline bool String16::operator<(const String16& other) const {
- return strzcmp16(mString, size(), other.mString, other.size()) < 0;
-}
-
-inline bool String16::operator<=(const String16& other) const {
- return strzcmp16(mString, size(), other.mString, other.size()) <= 0;
-}
-
-inline bool String16::operator==(const String16& other) const {
- return strzcmp16(mString, size(), other.mString, other.size()) == 0;
-}
-
-inline bool String16::operator!=(const String16& other) const {
- return strzcmp16(mString, size(), other.mString, other.size()) != 0;
-}
-
-inline bool String16::operator>=(const String16& other) const {
- return strzcmp16(mString, size(), other.mString, other.size()) >= 0;
-}
-
-inline bool String16::operator>(const String16& other) const {
- return strzcmp16(mString, size(), other.mString, other.size()) > 0;
-}
-
-inline bool String16::operator<(const char16_t* other) const {
- return strcmp16(mString, other) < 0;
-}
-
-inline bool String16::operator<=(const char16_t* other) const {
- return strcmp16(mString, other) <= 0;
-}
-
-inline bool String16::operator==(const char16_t* other) const {
- return strcmp16(mString, other) == 0;
-}
-
-inline bool String16::operator!=(const char16_t* other) const {
- return strcmp16(mString, other) != 0;
-}
-
-inline bool String16::operator>=(const char16_t* other) const {
- return strcmp16(mString, other) >= 0;
-}
-
-inline bool String16::operator>(const char16_t* other) const {
- return strcmp16(mString, other) > 0;
-}
-
-inline String16::operator const char16_t*() const { return mString; }
-
-} // namespace android
-
-// ---------------------------------------------------------------------------
-
-#endif // ANDROID_STRING16_H
diff --git a/nativeruntime/cpp/libutils/include/utils/String8.h b/nativeruntime/cpp/libutils/include/utils/String8.h
deleted file mode 100644
index af8d03c33..000000000
--- a/nativeruntime/cpp/libutils/include/utils/String8.h
+++ /dev/null
@@ -1,371 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/include/utils/String8.h
-
-#ifndef ANDROID_STRING8_H
-#define ANDROID_STRING8_H
-
-#include <stdarg.h>
-#include <string.h> // for strcmp
-#include <utils/Errors.h>
-#include <utils/TypeHelpers.h>
-#include <utils/Unicode.h>
-
-#include <string> // for std::string
-
-// ---------------------------------------------------------------------------
-
-namespace android {
-
-class String16;
-
-// DO NOT USE: please use std::string
-
-//! This is a string holding UTF-8 characters. Does not allow the value more
-// than 0x10FFFF, which is not valid unicode codepoint.
-class String8 {
- public:
- /* use String8(StaticLinkage) if you're statically linking against
- * libutils and declaring an empty static String8, e.g.:
- *
- * static String8 sAStaticEmptyString(String8::kEmptyString);
- * static String8 sAnotherStaticEmptyString(sAStaticEmptyString);
- */
- enum StaticLinkage { kEmptyString };
-
- String8();
- explicit String8(StaticLinkage);
- String8(const String8& o);
- explicit String8(const char* o);
- explicit String8(const char* o, size_t len);
-
- explicit String8(const String16& o);
- explicit String8(const char16_t* o);
- explicit String8(const char16_t* o, size_t len);
- explicit String8(const char32_t* o);
- explicit String8(const char32_t* o, size_t len);
- ~String8();
-
- static inline const String8 empty();
-
- static String8 format(const char* fmt, ...)
- __attribute__((format(printf, 1, 2)));
- static String8 formatV(const char* fmt, va_list args);
-
- inline const char* c_str() const;
- inline const char* string() const;
-
- private:
- static inline std::string std_string(const String8& str);
-
- public:
- inline size_t size() const;
- inline size_t bytes() const;
- inline bool isEmpty() const;
-
- size_t length() const;
-
- void clear();
-
- void setTo(const String8& other);
- status_t setTo(const char* other);
- status_t setTo(const char* other, size_t len);
- status_t setTo(const char16_t* other, size_t len);
- status_t setTo(const char32_t* other, size_t length);
-
- status_t append(const String8& other);
- status_t append(const char* other);
- status_t append(const char* other, size_t otherLen);
-
- status_t appendFormat(const char* fmt, ...)
- __attribute__((format(printf, 2, 3)));
- status_t appendFormatV(const char* fmt, va_list args);
-
- inline String8& operator=(const String8& other);
- inline String8& operator=(const char* other);
-
- inline String8& operator+=(const String8& other);
- inline String8 operator+(const String8& other) const;
-
- inline String8& operator+=(const char* other);
- inline String8 operator+(const char* other) const;
-
- inline int compare(const String8& other) const;
-
- inline bool operator<(const String8& other) const;
- inline bool operator<=(const String8& other) const;
- inline bool operator==(const String8& other) const;
- inline bool operator!=(const String8& other) const;
- inline bool operator>=(const String8& other) const;
- inline bool operator>(const String8& other) const;
-
- inline bool operator<(const char* other) const;
- inline bool operator<=(const char* other) const;
- inline bool operator==(const char* other) const;
- inline bool operator!=(const char* other) const;
- inline bool operator>=(const char* other) const;
- inline bool operator>(const char* other) const;
-
- inline operator const char*() const;
-
- char* lockBuffer(size_t size);
- void unlockBuffer();
- status_t unlockBuffer(size_t size);
-
- // return the index of the first byte of other in this at or after
- // start, or -1 if not found
- ssize_t find(const char* other, size_t start = 0) const;
-
- // return true if this string contains the specified substring
- inline bool contains(const char* other) const;
-
- // removes all occurrence of the specified substring
- // returns true if any were found and removed
- bool removeAll(const char* other);
-
- void toLower();
- void toLower(size_t start, size_t length);
- void toUpper();
- void toUpper(size_t start, size_t length);
-
- /*
- * These methods operate on the string as if it were a path name.
- */
-
- /*
- * Set the filename field to a specific value.
- *
- * Normalizes the filename, removing a trailing '/' if present.
- */
- void setPathName(const char* name);
- void setPathName(const char* name, size_t len);
-
- /*
- * Get just the filename component.
- *
- * "/tmp/foo/bar.c" --> "bar.c"
- */
- String8 getPathLeaf() const;
-
- /*
- * Remove the last (file name) component, leaving just the directory
- * name.
- *
- * "/tmp/foo/bar.c" --> "/tmp/foo"
- * "/tmp" --> "" // ????? shouldn't this be "/" ???? XXX
- * "bar.c" --> ""
- */
- String8 getPathDir() const;
-
- /*
- * Retrieve the front (root dir) component. Optionally also return the
- * remaining components.
- *
- * "/tmp/foo/bar.c" --> "tmp" (remain = "foo/bar.c")
- * "/tmp" --> "tmp" (remain = "")
- * "bar.c" --> "bar.c" (remain = "")
- */
- String8 walkPath(String8* outRemains = nullptr) const;
-
- /*
- * Return the filename extension. This is the last '.' and any number
- * of characters that follow it. The '.' is included in case we
- * decide to expand our definition of what constitutes an extension.
- *
- * "/tmp/foo/bar.c" --> ".c"
- * "/tmp" --> ""
- * "/tmp/foo.bar/baz" --> ""
- * "foo.jpeg" --> ".jpeg"
- * "foo." --> ""
- */
- String8 getPathExtension() const;
-
- /*
- * Return the path without the extension. Rules for what constitutes
- * an extension are described in the comment for getPathExtension().
- *
- * "/tmp/foo/bar.c" --> "/tmp/foo/bar"
- */
- String8 getBasePath() const;
-
- /*
- * Add a component to the pathname. We guarantee that there is
- * exactly one path separator between the old path and the new.
- * If there is no existing name, we just copy the new name in.
- *
- * If leaf is a fully qualified path (i.e. starts with '/', it
- * replaces whatever was there before.
- */
- String8& appendPath(const char* leaf);
- String8& appendPath(const String8& leaf) { return appendPath(leaf.string()); }
-
- /*
- * Like appendPath(), but does not affect this string. Returns a new one
- * instead.
- */
- String8 appendPathCopy(const char* leaf) const {
- String8 p(*this);
- p.appendPath(leaf);
- return p;
- }
- String8 appendPathCopy(const String8& leaf) const {
- return appendPathCopy(leaf.string());
- }
-
- /*
- * Converts all separators in this string to /, the default path separator.
- *
- * If the default OS separator is backslash, this converts all
- * backslashes to slashes, in-place. Otherwise it does nothing.
- * Returns self.
- */
- String8& convertToResPath();
-
- private:
- status_t real_append(const char* other, size_t otherLen);
- char* find_extension() const;
-
- const char* mString;
-};
-
-// String8 can be trivially moved using memcpy() because moving does not
-// require any change to the underlying SharedBuffer contents or reference
-// count.
-ANDROID_TRIVIAL_MOVE_TRAIT(String8)
-
-// ---------------------------------------------------------------------------
-// No user servicable parts below.
-
-inline int compare_type(const String8& lhs, const String8& rhs) {
- return lhs.compare(rhs);
-}
-
-inline int strictly_order_type(const String8& lhs, const String8& rhs) {
- return compare_type(lhs, rhs) < 0;
-}
-
-inline const String8 String8::empty() { return String8(); }
-
-inline const char* String8::c_str() const { return mString; }
-inline const char* String8::string() const { return mString; }
-
-inline std::string String8::std_string(const String8& str) {
- return std::string(str.string());
-}
-
-inline size_t String8::size() const { return length(); }
-
-inline bool String8::isEmpty() const { return length() == 0; }
-
-inline size_t String8::bytes() const { return length(); }
-
-inline bool String8::contains(const char* other) const {
- return find(other) >= 0;
-}
-
-inline String8& String8::operator=(const String8& other) {
- setTo(other);
- return *this;
-}
-
-inline String8& String8::operator=(const char* other) {
- setTo(other);
- return *this;
-}
-
-inline String8& String8::operator+=(const String8& other) {
- append(other);
- return *this;
-}
-
-inline String8 String8::operator+(const String8& other) const {
- String8 tmp(*this);
- tmp += other;
- return tmp;
-}
-
-inline String8& String8::operator+=(const char* other) {
- append(other);
- return *this;
-}
-
-inline String8 String8::operator+(const char* other) const {
- String8 tmp(*this);
- tmp += other;
- return tmp;
-}
-
-inline int String8::compare(const String8& other) const {
- return strcmp(mString, other.mString);
-}
-
-inline bool String8::operator<(const String8& other) const {
- return strcmp(mString, other.mString) < 0;
-}
-
-inline bool String8::operator<=(const String8& other) const {
- return strcmp(mString, other.mString) <= 0;
-}
-
-inline bool String8::operator==(const String8& other) const {
- return strcmp(mString, other.mString) == 0;
-}
-
-inline bool String8::operator!=(const String8& other) const {
- return strcmp(mString, other.mString) != 0;
-}
-
-inline bool String8::operator>=(const String8& other) const {
- return strcmp(mString, other.mString) >= 0;
-}
-
-inline bool String8::operator>(const String8& other) const {
- return strcmp(mString, other.mString) > 0;
-}
-
-inline bool String8::operator<(const char* other) const {
- return strcmp(mString, other) < 0;
-}
-
-inline bool String8::operator<=(const char* other) const {
- return strcmp(mString, other) <= 0;
-}
-
-inline bool String8::operator==(const char* other) const {
- return strcmp(mString, other) == 0;
-}
-
-inline bool String8::operator!=(const char* other) const {
- return strcmp(mString, other) != 0;
-}
-
-inline bool String8::operator>=(const char* other) const {
- return strcmp(mString, other) >= 0;
-}
-
-inline bool String8::operator>(const char* other) const {
- return strcmp(mString, other) > 0;
-}
-
-inline String8::operator const char*() const { return mString; }
-
-} // namespace android
-
-// ---------------------------------------------------------------------------
-
-#endif // ANDROID_STRING8_H
diff --git a/nativeruntime/cpp/libutils/include/utils/TypeHelpers.h b/nativeruntime/cpp/libutils/include/utils/TypeHelpers.h
deleted file mode 100644
index 9c5b14d11..000000000
--- a/nativeruntime/cpp/libutils/include/utils/TypeHelpers.h
+++ /dev/null
@@ -1,370 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/include/utils/TypeHelpers.h
-
-#ifndef ANDROID_TYPE_HELPERS_H
-#define ANDROID_TYPE_HELPERS_H
-
-#include <stdint.h>
-#include <string.h>
-#include <sys/types.h>
-
-#include <new>
-#include <type_traits>
-
-// ---------------------------------------------------------------------------
-
-namespace android {
-
-/*
- * Types traits
- */
-
-template <typename T>
-struct trait_trivial_ctor {
- enum { value = false };
-};
-template <typename T>
-struct trait_trivial_dtor {
- enum { value = false };
-};
-template <typename T>
-struct trait_trivial_copy {
- enum { value = false };
-};
-template <typename T>
-struct trait_trivial_move {
- enum { value = false };
-};
-template <typename T>
-struct trait_pointer {
- enum { value = false };
-};
-template <typename T>
-struct trait_pointer<T*> {
- enum { value = true };
-};
-
-template <typename TYPE>
-struct traits {
- enum {
- // whether this type is a pointer
- is_pointer = trait_pointer<TYPE>::value,
- // whether this type's constructor is a no-op
- has_trivial_ctor = is_pointer || trait_trivial_ctor<TYPE>::value,
- // whether this type's destructor is a no-op
- has_trivial_dtor = is_pointer || trait_trivial_dtor<TYPE>::value,
- // whether this type type can be copy-constructed with memcpy
- has_trivial_copy = is_pointer || trait_trivial_copy<TYPE>::value,
- // whether this type can be moved with memmove
- has_trivial_move = is_pointer || trait_trivial_move<TYPE>::value
- };
-};
-
-template <typename T, typename U>
-struct aggregate_traits {
- enum {
- is_pointer = false,
- has_trivial_ctor =
- traits<T>::has_trivial_ctor && traits<U>::has_trivial_ctor,
- has_trivial_dtor =
- traits<T>::has_trivial_dtor && traits<U>::has_trivial_dtor,
- has_trivial_copy =
- traits<T>::has_trivial_copy && traits<U>::has_trivial_copy,
- has_trivial_move =
- traits<T>::has_trivial_move && traits<U>::has_trivial_move
- };
-};
-
-#define ANDROID_TRIVIAL_CTOR_TRAIT(T) \
- template <> \
- struct trait_trivial_ctor<T> { \
- enum { value = true }; \
- };
-
-#define ANDROID_TRIVIAL_DTOR_TRAIT(T) \
- template <> \
- struct trait_trivial_dtor<T> { \
- enum { value = true }; \
- };
-
-#define ANDROID_TRIVIAL_COPY_TRAIT(T) \
- template <> \
- struct trait_trivial_copy<T> { \
- enum { value = true }; \
- };
-
-#define ANDROID_TRIVIAL_MOVE_TRAIT(T) \
- template <> \
- struct trait_trivial_move<T> { \
- enum { value = true }; \
- };
-
-#define ANDROID_BASIC_TYPES_TRAITS(T) \
- ANDROID_TRIVIAL_CTOR_TRAIT(T) \
- ANDROID_TRIVIAL_DTOR_TRAIT(T) \
- ANDROID_TRIVIAL_COPY_TRAIT(T) \
- ANDROID_TRIVIAL_MOVE_TRAIT(T)
-
-// ---------------------------------------------------------------------------
-
-/*
- * basic types traits
- */
-
-ANDROID_BASIC_TYPES_TRAITS(void)
-ANDROID_BASIC_TYPES_TRAITS(bool)
-ANDROID_BASIC_TYPES_TRAITS(char)
-ANDROID_BASIC_TYPES_TRAITS(unsigned char)
-ANDROID_BASIC_TYPES_TRAITS(short)
-ANDROID_BASIC_TYPES_TRAITS(unsigned short)
-ANDROID_BASIC_TYPES_TRAITS(int)
-ANDROID_BASIC_TYPES_TRAITS(unsigned int)
-ANDROID_BASIC_TYPES_TRAITS(long)
-ANDROID_BASIC_TYPES_TRAITS(unsigned long)
-ANDROID_BASIC_TYPES_TRAITS(long long)
-ANDROID_BASIC_TYPES_TRAITS(unsigned long long)
-ANDROID_BASIC_TYPES_TRAITS(float)
-ANDROID_BASIC_TYPES_TRAITS(double)
-
-// ---------------------------------------------------------------------------
-
-/*
- * compare and order types
- */
-
-template <typename TYPE>
-inline int strictly_order_type(const TYPE& lhs, const TYPE& rhs) {
- return (lhs < rhs) ? 1 : 0;
-}
-
-template <typename TYPE>
-inline int compare_type(const TYPE& lhs, const TYPE& rhs) {
- return strictly_order_type(rhs, lhs) - strictly_order_type(lhs, rhs);
-}
-
-/*
- * create, destroy, copy and move types...
- */
-
-template <typename TYPE>
-inline void construct_type(TYPE* p, size_t n) {
- if (!traits<TYPE>::has_trivial_ctor) {
- while (n > 0) {
- n--;
- new (p++) TYPE;
- }
- }
-}
-
-template <typename TYPE>
-inline void destroy_type(TYPE* p, size_t n) {
- if (!traits<TYPE>::has_trivial_dtor) {
- while (n > 0) {
- n--;
- p->~TYPE();
- p++;
- }
- }
-}
-
-template <typename TYPE>
-typename std::enable_if<traits<TYPE>::has_trivial_copy>::type inline copy_type(
- TYPE* d, const TYPE* s, size_t n) {
- memcpy(d, s, n * sizeof(TYPE));
-}
-
-template <typename TYPE>
-typename std::enable_if<!traits<TYPE>::has_trivial_copy>::type inline copy_type(
- TYPE* d, const TYPE* s, size_t n) {
- while (n > 0) {
- n--;
- new (d) TYPE(*s);
- d++, s++;
- }
-}
-
-template <typename TYPE>
-inline void splat_type(TYPE* where, const TYPE* what, size_t n) {
- if (!traits<TYPE>::has_trivial_copy) {
- while (n > 0) {
- n--;
- new (where) TYPE(*what);
- where++;
- }
- } else {
- while (n > 0) {
- n--;
- *where++ = *what;
- }
- }
-}
-
-template <typename TYPE>
-struct use_trivial_move
- : public std::integral_constant<bool, (traits<TYPE>::has_trivial_dtor &&
- traits<TYPE>::has_trivial_copy) ||
- traits<TYPE>::has_trivial_move> {
-};
-
-template <typename TYPE>
-typename std::enable_if<use_trivial_move<TYPE>::value>::
- type inline move_forward_type(TYPE* d, const TYPE* s, size_t n = 1) {
- memmove(d, s, n * sizeof(TYPE));
-}
-
-template <typename TYPE>
-typename std::enable_if<!use_trivial_move<TYPE>::value>::
- type inline move_forward_type(TYPE* d, const TYPE* s, size_t n = 1) {
- d += n;
- s += n;
- while (n > 0) {
- n--;
- --d, --s;
- if (!traits<TYPE>::has_trivial_copy) {
- new (d) TYPE(*s);
- } else {
- *d = *s;
- }
- if (!traits<TYPE>::has_trivial_dtor) {
- s->~TYPE();
- }
- }
-}
-
-template <typename TYPE>
-typename std::enable_if<use_trivial_move<TYPE>::value>::
- type inline move_backward_type(TYPE* d, const TYPE* s, size_t n = 1) {
- memmove(d, s, n * sizeof(TYPE));
-}
-
-template <typename TYPE>
-typename std::enable_if<!use_trivial_move<TYPE>::value>::
- type inline move_backward_type(TYPE* d, const TYPE* s, size_t n = 1) {
- while (n > 0) {
- n--;
- if (!traits<TYPE>::has_trivial_copy) {
- new (d) TYPE(*s);
- } else {
- *d = *s;
- }
- if (!traits<TYPE>::has_trivial_dtor) {
- s->~TYPE();
- }
- d++, s++;
- }
-}
-
-// ---------------------------------------------------------------------------
-
-/*
- * a key/value pair
- */
-
-template <typename KEY, typename VALUE>
-struct key_value_pair_t {
- typedef KEY key_t;
- typedef VALUE value_t;
-
- KEY key;
- VALUE value;
- key_value_pair_t() {}
- key_value_pair_t(const key_value_pair_t& o) : key(o.key), value(o.value) {}
- key_value_pair_t& operator=(const key_value_pair_t& o) {
- key = o.key;
- value = o.value;
- return *this;
- }
- key_value_pair_t(const KEY& k, const VALUE& v) : key(k), value(v) {}
- explicit key_value_pair_t(const KEY& k) : key(k) {}
- inline bool operator<(const key_value_pair_t& o) const {
- return strictly_order_type(key, o.key);
- }
- inline const KEY& getKey() const { return key; }
- inline const VALUE& getValue() const { return value; }
-};
-
-template <typename K, typename V>
-struct trait_trivial_ctor<key_value_pair_t<K, V> > {
- enum { value = aggregate_traits<K, V>::has_trivial_ctor };
-};
-template <typename K, typename V>
-struct trait_trivial_dtor<key_value_pair_t<K, V> > {
- enum { value = aggregate_traits<K, V>::has_trivial_dtor };
-};
-template <typename K, typename V>
-struct trait_trivial_copy<key_value_pair_t<K, V> > {
- enum { value = aggregate_traits<K, V>::has_trivial_copy };
-};
-template <typename K, typename V>
-struct trait_trivial_move<key_value_pair_t<K, V> > {
- enum { value = aggregate_traits<K, V>::has_trivial_move };
-};
-
-// ---------------------------------------------------------------------------
-
-/*
- * Hash codes.
- */
-typedef uint32_t hash_t;
-
-template <typename TKey>
-hash_t hash_type(const TKey& key);
-
-/* Built-in hash code specializations */
-#define ANDROID_INT32_HASH(T) \
- template <> \
- inline hash_t hash_type(const T& value) { \
- return hash_t(value); \
- }
-#define ANDROID_INT64_HASH(T) \
- template <> \
- inline hash_t hash_type(const T& value) { \
- return hash_t((value >> 32) ^ value); \
- }
-#define ANDROID_REINTERPRET_HASH(T, R) \
- template <> \
- inline hash_t hash_type(const T& value) { \
- R newValue; \
- static_assert(sizeof(newValue) == sizeof(value), "size mismatch"); \
- memcpy(&newValue, &value, sizeof(newValue)); \
- return hash_type(newValue); \
- }
-
-ANDROID_INT32_HASH(bool)
-ANDROID_INT32_HASH(int8_t)
-ANDROID_INT32_HASH(uint8_t)
-ANDROID_INT32_HASH(int16_t)
-ANDROID_INT32_HASH(uint16_t)
-ANDROID_INT32_HASH(int32_t)
-ANDROID_INT32_HASH(uint32_t)
-ANDROID_INT64_HASH(int64_t)
-ANDROID_INT64_HASH(uint64_t)
-ANDROID_REINTERPRET_HASH(float, uint32_t)
-ANDROID_REINTERPRET_HASH(double, uint64_t)
-
-template <typename T>
-inline hash_t hash_type(T* const& value) {
- return hash_type(uintptr_t(value));
-}
-
-} // namespace android
-
-// ---------------------------------------------------------------------------
-
-#endif // ANDROID_TYPE_HELPERS_H
diff --git a/nativeruntime/cpp/libutils/include/utils/Unicode.h b/nativeruntime/cpp/libutils/include/utils/Unicode.h
deleted file mode 100644
index dfd73f3e0..000000000
--- a/nativeruntime/cpp/libutils/include/utils/Unicode.h
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2005 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Derived from
-// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:system/core/libutils/include/utils/Unicode.h
-
-#ifndef ANDROID_UNICODE_H
-#define ANDROID_UNICODE_H
-
-#include <stdint.h>
-#include <sys/types.h>
-
-extern "C" {
-
-// Standard string functions on char16_t strings.
-int strcmp16(const char16_t *, const char16_t *);
-int strncmp16(const char16_t *s1, const char16_t *s2, size_t n);
-size_t strlen16(const char16_t *);
-size_t strnlen16(const char16_t *, size_t);
-char16_t *strcpy16(char16_t *, const char16_t *);
-char16_t *strstr16(const char16_t *, const char16_t *);
-
-// Version of comparison that supports embedded NULs.
-// This is different than strncmp() because we don't stop
-// at a nul character and consider the strings to be different
-// if the lengths are different (thus we need to supply the
-// lengths of both strings). This can also be used when
-// your string is not nul-terminated as it will have the
-// equivalent result as strcmp16 (unlike strncmp16).
-int strzcmp16(const char16_t *s1, size_t n1, const char16_t *s2, size_t n2);
-
-// Standard string functions on char32_t strings.
-size_t strlen32(const char32_t *);
-size_t strnlen32(const char32_t *, size_t);
-
-/**
- * Measure the length of a UTF-32 string in UTF-8. If the string is invalid
- * such as containing a surrogate character, -1 will be returned.
- */
-ssize_t utf32_to_utf8_length(const char32_t *src, size_t src_len);
-
-/**
- * Stores a UTF-8 string converted from "src" in "dst", if "dst_length" is not
- * large enough to store the string, the part of the "src" string is stored
- * into "dst" as much as possible. See the examples for more detail.
- * Returns the size actually used for storing the string.
- * dst" is not nul-terminated when dst_len is fully used (like strncpy).
- *
- * \code
- * Example 1
- * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84)
- * "src_len" == 2
- * "dst_len" >= 7
- * ->
- * Returned value == 6
- * "dst" becomes \xE3\x81\x82\xE3\x81\x84\0
- * (note that "dst" is nul-terminated)
- *
- * Example 2
- * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84)
- * "src_len" == 2
- * "dst_len" == 5
- * ->
- * Returned value == 3
- * "dst" becomes \xE3\x81\x82\0
- * (note that "dst" is nul-terminated, but \u3044 is not stored in "dst"
- * since "dst" does not have enough size to store the character)
- *
- * Example 3
- * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84)
- * "src_len" == 2
- * "dst_len" == 6
- * ->
- * Returned value == 6
- * "dst" becomes \xE3\x81\x82\xE3\x81\x84
- * (note that "dst" is NOT nul-terminated, like strncpy)
- * \endcode
- */
-void utf32_to_utf8(const char32_t *src, size_t src_len, char *dst,
- size_t dst_len);
-
-/**
- * Returns the unicode value at "index".
- * Returns -1 when the index is invalid (equals to or more than "src_len").
- * If returned value is positive, it is able to be converted to char32_t, which
- * is unsigned. Then, if "next_index" is not NULL, the next index to be used is
- * stored in "next_index". "next_index" can be NULL.
- */
-int32_t utf32_from_utf8_at(const char *src, size_t src_len, size_t index,
- size_t *next_index);
-
-/**
- * Returns the UTF-8 length of UTF-16 string "src".
- */
-ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len);
-
-/**
- * Converts a UTF-16 string to UTF-8. The destination buffer must be large
- * enough to fit the UTF-16 as measured by utf16_to_utf8_length with an added
- * NUL terminator.
- */
-void utf16_to_utf8(const char16_t *src, size_t src_len, char *dst,
- size_t dst_len);
-
-/**
- * Returns the length of "src" when "src" is valid UTF-8 string.
- * Returns 0 if src is NULL or 0-length string. Returns -1 when the source
- * is an invalid string.
- *
- * This function should be used to determine whether "src" is valid UTF-8
- * characters with valid unicode codepoints. "src" must be nul-terminated.
- *
- * If you are going to use other utf8_to_... functions defined in this header
- * with string which may not be valid UTF-8 with valid codepoint (form 0 to
- * 0x10FFFF), you should use this function before calling others, since the
- * other functions do not check whether the string is valid UTF-8 or not.
- *
- * If you do not care whether "src" is valid UTF-8 or not, you should use
- * strlen() as usual, which should be much faster.
- */
-ssize_t utf8_length(const char *src);
-
-/**
- * Returns the UTF-16 length of UTF-8 string "src". Returns -1 in case
- * it's invalid utf8. No buffer over-read occurs because of bound checks. Using
- * overreadIsFatal you can ask to log a message and fail in case the invalid
- * utf8 could have caused an override if no bound checks were used (otherwise -1
- * is returned).
- */
-ssize_t utf8_to_utf16_length(const uint8_t *u8str, size_t u8len,
- bool overreadIsFatal = false);
-
-/**
- * Convert UTF-8 to UTF-16 including surrogate pairs.
- * Returns a pointer to the end of the string (where a NUL terminator might go
- * if you wanted to add one). At most dstLen characters are written; it won't
- * emit half a surrogate pair. If dstLen == 0 nothing is written and dst is
- * returned. If dstLen > SSIZE_MAX it aborts (this being probably a negative
- * number returned as an error and casted to unsigned).
- */
-char16_t *utf8_to_utf16_no_null_terminator(const uint8_t *src, size_t srcLen,
- char16_t *dst, size_t dstLen);
-
-/**
- * Convert UTF-8 to UTF-16 including surrogate pairs. At most dstLen - 1
- * characters are written; it won't emit half a surrogate pair; and a NUL
- * terminator is appended after. dstLen - 1 can be measured beforehand using
- * utf8_to_utf16_length. Aborts if dstLen == 0 (at least one character is needed
- * for the NUL terminator) or dstLen > SSIZE_MAX (the latter case being likely a
- * negative number returned as an error and casted to unsigned) . Returns a
- * pointer to the NUL terminator.
- */
-char16_t *utf8_to_utf16(const uint8_t *u8str, size_t u8len, char16_t *u16str,
- size_t u16len);
-}
-
-#endif
diff --git a/nativeruntime/external/icu b/nativeruntime/external/icu
deleted file mode 160000
-Subproject 0e7b4428866f3133b4abba2d932ee3faa708db1
diff --git a/nativeruntime/external/sqlite b/nativeruntime/external/sqlite
deleted file mode 160000
-Subproject 41e1a3604e32f92120a0d4ce2d62c4a5c8f8673
diff --git a/plugins/maven-dependency-resolver/build.gradle b/plugins/maven-dependency-resolver/build.gradle
index 48e799a05..de20b2b8f 100644
--- a/plugins/maven-dependency-resolver/build.gradle
+++ b/plugins/maven-dependency-resolver/build.gradle
@@ -3,13 +3,55 @@ import org.robolectric.gradle.RoboJavaModulePlugin
apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
+apply plugin: 'kotlin'
+apply plugin: "com.diffplug.spotless"
+
+spotless {
+ kotlin {
+ target '**/*.kt'
+ ktfmt('0.42').googleStyle()
+ }
+}
+
+tasks.withType(GenerateModuleMetadata) {
+ // We don't want to release gradle module metadata now to avoid
+ // potential compatibility problems.
+ enabled = false
+}
+
+compileKotlin {
+ // Use java/main classes directory to replace default kotlin/main to
+ // avoid d8 error when dexing & desugaring kotlin classes with non-exist
+ // kotlin/main directory because utils module doesn't have kotlin code
+ // in production. If utils module starts to add Kotlin code in main source
+ // set, we can remove this destinationDirectory modification.
+ destinationDirectory = file("${projectDir}/build/classes/java/main")
+ compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+}
+
+afterEvaluate {
+ configurations {
+ runtimeElements {
+ attributes {
+ // We should add artifactType with jar to ensure standard runtimeElements variant
+ // has a max priority selection sequence than other variants that brought by
+ // kotlin plugin.
+ attribute(
+ Attribute.of("artifactType", String.class),
+ ArtifactTypeDefinition.JAR_TYPE
+ )
+ }
+ }
+ }
+}
dependencies {
api project(":pluginapi")
api project(":utils")
api "com.google.guava:guava:$guavaJREVersion"
- testImplementation "junit:junit:${junitVersion}"
- testImplementation "org.mockito:mockito-core:${mockitoVersion}"
- testImplementation "com.google.truth:truth:${truthVersion}"
+ testImplementation "junit:junit:$junitVersion"
+ testImplementation "org.mockito:mockito-core:$mockitoVersion"
+ testImplementation "com.google.truth:truth:$truthVersion"
+ testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
}
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
deleted file mode 100644
index 8924257d1..000000000
--- a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package org.robolectric;
-
-import static org.junit.Assert.assertEquals;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public class MavenRoboSettingsTest {
-
- private String originalMavenRepositoryId;
- private String originalMavenRepositoryUrl;
- private String originalMavenRepositoryUserName;
- private String originalMavenRepositoryPassword;
- private String originalMavenRepositoryProxyHost;
- private int originalMavenProxyPort;
-
- @Before
- public void setUp() {
- originalMavenRepositoryId = MavenRoboSettings.getMavenRepositoryId();
- originalMavenRepositoryUrl = MavenRoboSettings.getMavenRepositoryUrl();
- originalMavenRepositoryUserName = MavenRoboSettings.getMavenRepositoryUserName();
- originalMavenRepositoryPassword = MavenRoboSettings.getMavenRepositoryPassword();
- originalMavenRepositoryProxyHost = MavenRoboSettings.getMavenProxyHost();
- originalMavenProxyPort = MavenRoboSettings.getMavenProxyPort();
- }
-
- @After
- public void tearDown() {
- MavenRoboSettings.setMavenRepositoryId(originalMavenRepositoryId);
- MavenRoboSettings.setMavenRepositoryUrl(originalMavenRepositoryUrl);
- MavenRoboSettings.setMavenRepositoryUserName(originalMavenRepositoryUserName);
- MavenRoboSettings.setMavenRepositoryPassword(originalMavenRepositoryPassword);
- MavenRoboSettings.setMavenProxyHost(originalMavenRepositoryProxyHost);
- MavenRoboSettings.setMavenProxyPort(originalMavenProxyPort);
- }
-
- @Test
- public void getMavenRepositoryId_defaultSonatype() {
- assertEquals("mavenCentral", MavenRoboSettings.getMavenRepositoryId());
- }
-
- @Test
- public void setMavenRepositoryId() {
- MavenRoboSettings.setMavenRepositoryId("testRepo");
- assertEquals("testRepo", MavenRoboSettings.getMavenRepositoryId());
- }
-
- @Test
- public void getMavenRepositoryUrl_defaultSonatype() {
- assertEquals("https://repo1.maven.org/maven2", MavenRoboSettings.getMavenRepositoryUrl());
- }
-
- @Test
- public void setMavenRepositoryUrl() {
- MavenRoboSettings.setMavenRepositoryUrl("http://local");
- assertEquals("http://local", MavenRoboSettings.getMavenRepositoryUrl());
- }
-
- @Test
- public void setMavenRepositoryUserName() {
- MavenRoboSettings.setMavenRepositoryUserName("username");
- assertEquals("username", MavenRoboSettings.getMavenRepositoryUserName());
- }
-
- @Test
- public void setMavenRepositoryPassword() {
- 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/MavenRoboSettingsTest.kt b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.kt
new file mode 100644
index 000000000..da7d56ac9
--- /dev/null
+++ b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/MavenRoboSettingsTest.kt
@@ -0,0 +1,83 @@
+package org.robolectric
+
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MavenRoboSettingsTest {
+ private var originalMavenRepositoryId: String? = null
+ private var originalMavenRepositoryUrl: String? = null
+ private var originalMavenRepositoryUserName: String? = null
+ private var originalMavenRepositoryPassword: String? = null
+ private var originalMavenRepositoryProxyHost: String? = null
+ private var originalMavenProxyPort = 0
+ @Before
+ fun setUp() {
+ originalMavenRepositoryId = MavenRoboSettings.getMavenRepositoryId()
+ originalMavenRepositoryUrl = MavenRoboSettings.getMavenRepositoryUrl()
+ originalMavenRepositoryUserName = MavenRoboSettings.getMavenRepositoryUserName()
+ originalMavenRepositoryPassword = MavenRoboSettings.getMavenRepositoryPassword()
+ originalMavenRepositoryProxyHost = MavenRoboSettings.getMavenProxyHost()
+ originalMavenProxyPort = MavenRoboSettings.getMavenProxyPort()
+ }
+
+ @After
+ fun tearDown() {
+ MavenRoboSettings.setMavenRepositoryId(originalMavenRepositoryId)
+ MavenRoboSettings.setMavenRepositoryUrl(originalMavenRepositoryUrl)
+ MavenRoboSettings.setMavenRepositoryUserName(originalMavenRepositoryUserName)
+ MavenRoboSettings.setMavenRepositoryPassword(originalMavenRepositoryPassword)
+ MavenRoboSettings.setMavenProxyHost(originalMavenRepositoryProxyHost)
+ MavenRoboSettings.setMavenProxyPort(originalMavenProxyPort)
+ }
+
+ @Test
+ fun mavenRepositoryId_defaultSonatype() {
+ assertEquals("mavenCentral", MavenRoboSettings.getMavenRepositoryId())
+ }
+
+ @Test
+ fun setMavenRepositoryId() {
+ MavenRoboSettings.setMavenRepositoryId("testRepo")
+ assertEquals("testRepo", MavenRoboSettings.getMavenRepositoryId())
+ }
+
+ @Test
+ fun mavenRepositoryUrl_defaultSonatype() {
+ assertEquals("https://repo1.maven.org/maven2", MavenRoboSettings.getMavenRepositoryUrl())
+ }
+
+ @Test
+ fun setMavenRepositoryUrl() {
+ MavenRoboSettings.setMavenRepositoryUrl("http://local")
+ assertEquals("http://local", MavenRoboSettings.getMavenRepositoryUrl())
+ }
+
+ @Test
+ fun setMavenRepositoryUserName() {
+ MavenRoboSettings.setMavenRepositoryUserName("username")
+ assertEquals("username", MavenRoboSettings.getMavenRepositoryUserName())
+ }
+
+ @Test
+ fun setMavenRepositoryPassword() {
+ MavenRoboSettings.setMavenRepositoryPassword("password")
+ assertEquals("password", MavenRoboSettings.getMavenRepositoryPassword())
+ }
+
+ @Test
+ fun setMavenProxyHost() {
+ MavenRoboSettings.setMavenProxyHost("123.4.5.678")
+ assertEquals("123.4.5.678", MavenRoboSettings.getMavenProxyHost())
+ }
+
+ @Test
+ fun setMavenProxyPort() {
+ MavenRoboSettings.setMavenProxyPort(9000)
+ assertEquals(9000, MavenRoboSettings.getMavenProxyPort().toLong())
+ }
+}
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
deleted file mode 100644
index f438414d3..000000000
--- a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.java
+++ /dev/null
@@ -1,291 +0,0 @@
-package org.robolectric.internal.dependency;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
-import com.google.common.hash.HashFunction;
-import com.google.common.hash.Hashing;
-import com.google.common.io.Files;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-import java.io.File;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.ExecutorService;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-@SuppressWarnings("UnstableApiUsage")
-public class MavenDependencyResolverTest {
- private static final File REPOSITORY_DIR;
- 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 =
- new DependencyJar[] {
- new DependencyJar("group", "artifact", "1"),
- new DependencyJar("org.group2", "artifact2-name", "2.4.5"),
- new DependencyJar("org.robolectric", "android-all", "10-robolectric-5803371"),
- };
-
- static {
- try {
- REPOSITORY_DIR = Files.createTempDir();
- REPOSITORY_DIR.deleteOnExit();
- REPOSITORY_URL = REPOSITORY_DIR.toURI().toURL().toString();
-
- for (DependencyJar dependencyJar : successCases) {
- addTestArtifact(dependencyJar);
- }
- } catch (Exception e) {
- throw new AssertionError(e);
- }
- }
-
- private File localRepositoryDir;
- private ExecutorService executorService;
- private MavenDependencyResolver mavenDependencyResolver;
- private TestMavenArtifactFetcher mavenArtifactFetcher;
-
- @Before
- public void setUp() throws Exception {
- executorService = MoreExecutors.newDirectExecutorService();
- localRepositoryDir = Files.createTempDir();
- localRepositoryDir.deleteOnExit();
- mavenArtifactFetcher =
- new TestMavenArtifactFetcher(
- REPOSITORY_URL,
- REPOSITORY_USERNAME,
- REPOSITORY_PASSWORD,
- PROXY_HOST,
- PROXY_PORT,
- localRepositoryDir,
- executorService);
- mavenDependencyResolver = new TestMavenDependencyResolver();
- }
-
- @Test
- public void getLocalArtifactUrl_placesFilesCorrectlyForSingleURL() throws Exception {
- DependencyJar dependencyJar = successCases[0];
- mavenDependencyResolver.getLocalArtifactUrl(dependencyJar);
- assertThat(mavenArtifactFetcher.getNumRequests()).isEqualTo(4);
- MavenJarArtifact artifact = new MavenJarArtifact(dependencyJar);
- checkJarArtifact(artifact);
- }
-
- @Test
- public void getLocalArtifactUrl_placesFilesCorrectlyForMultipleURL() throws Exception {
- mavenDependencyResolver.getLocalArtifactUrls(successCases);
- assertThat(mavenArtifactFetcher.getNumRequests()).isEqualTo(4 * successCases.length);
- for (DependencyJar dependencyJar : successCases) {
- MavenJarArtifact artifact = new MavenJarArtifact(dependencyJar);
- checkJarArtifact(artifact);
- }
- }
-
- /** Checks the case where the existing artifact directory is valid. */
- @Test
- public void getLocalArtifactUrl_handlesExistingArtifactDirectory() throws Exception {
- DependencyJar dependencyJar = new DependencyJar("group", "artifact", "1");
- MavenJarArtifact mavenJarArtifact = new MavenJarArtifact(dependencyJar);
- File jarFile = new File(localRepositoryDir, mavenJarArtifact.jarPath());
- Files.createParentDirs(jarFile);
- assertThat(jarFile.getParentFile().isDirectory()).isTrue();
- mavenDependencyResolver.getLocalArtifactUrl(dependencyJar);
- checkJarArtifact(mavenJarArtifact);
- }
-
- /**
- * Checks the case where there is some existing artifact metadata in the artifact directory, but
- * not the JAR.
- */
- @Test
- public void getLocalArtifactUrl_handlesExistingMetadataFile() throws Exception {
- DependencyJar dependencyJar = new DependencyJar("group", "artifact", "1");
- MavenJarArtifact mavenJarArtifact = new MavenJarArtifact(dependencyJar);
- File pomFile = new File(localRepositoryDir, mavenJarArtifact.pomPath());
- pomFile.getParentFile().mkdirs();
- Files.write(new byte[0], pomFile);
- assertThat(pomFile.exists()).isTrue();
- mavenDependencyResolver.getLocalArtifactUrl(dependencyJar);
- checkJarArtifact(mavenJarArtifact);
- }
-
- private void checkJarArtifact(MavenJarArtifact artifact) throws Exception {
- File jar = new File(localRepositoryDir, artifact.jarPath());
- File pom = new File(localRepositoryDir, artifact.pomPath());
- File jarSha512 = new File(localRepositoryDir, artifact.jarSha512Path());
- File pomSha512 = new File(localRepositoryDir, artifact.pomSha512Path());
- assertThat(jar.exists()).isTrue();
- assertThat(readFile(jar)).isEqualTo(artifact.toString() + " jar contents");
- assertThat(pom.exists()).isTrue();
- assertThat(readFile(pom)).isEqualTo(artifact.toString() + " pom contents");
- assertThat(jarSha512.exists()).isTrue();
- assertThat(readFile(jarSha512)).isEqualTo(sha512(artifact.toString() + " jar contents"));
- assertThat(pom.exists()).isTrue();
- assertThat(readFile(pomSha512)).isEqualTo(sha512(artifact.toString() + " pom contents"));
- }
-
- @Test
- public void getLocalArtifactUrl_doesNotFetchWhenArtifactsExist() throws Exception {
- DependencyJar dependencyJar = new DependencyJar("group", "artifact", "1");
- MavenJarArtifact mavenJarArtifact = new MavenJarArtifact(dependencyJar);
- File artifactFile = new File(localRepositoryDir, mavenJarArtifact.jarPath());
- artifactFile.getParentFile().mkdirs();
- Files.write(new byte[0], artifactFile);
- assertThat(artifactFile.exists()).isTrue();
- mavenDependencyResolver.getLocalArtifactUrl(dependencyJar);
- assertThat(mavenArtifactFetcher.getNumRequests()).isEqualTo(0);
- }
-
- @Test
- public void getLocalArtifactUrl_handlesFileNotFound() throws Exception {
- DependencyJar dependencyJar = new DependencyJar("group", "missing-artifact", "1");
-
- assertThrows(
- AssertionError.class, () -> mavenDependencyResolver.getLocalArtifactUrl(dependencyJar));
- }
-
- @Test
- public void getLocalArtifactUrl_handlesInvalidSha512() throws Exception {
- DependencyJar dependencyJar = new DependencyJar("group", "artifact-invalid-sha512", "1");
- addTestArtifactInvalidSha512(dependencyJar);
- assertThrows(
- AssertionError.class, () -> mavenDependencyResolver.getLocalArtifactUrl(dependencyJar));
- }
-
- class TestMavenDependencyResolver extends MavenDependencyResolver {
-
- @Override
- protected MavenArtifactFetcher createMavenFetcher(
- String repositoryUrl,
- String repositoryUserName,
- String repositoryPassword,
- String proxyHost,
- int proxyPort,
- File localRepositoryDir,
- ExecutorService executorService) {
- return mavenArtifactFetcher;
- }
-
- @Override
- protected ExecutorService createExecutorService() {
- return executorService;
- }
-
- @Override
- protected File getLocalRepositoryDir() {
- return localRepositoryDir;
- }
-
- @Override
- protected File createLockFile() {
- try {
- return File.createTempFile("MavenDependencyResolverTest", null);
- } catch (IOException e) {
- throw new AssertionError(e);
- }
- }
- }
-
- static class TestMavenArtifactFetcher extends MavenArtifactFetcher {
- private ExecutorService executorService;
- private int numRequests;
-
- public TestMavenArtifactFetcher(
- 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;
- }
-
- @Override
- protected ListenableFuture<Void> createFetchToFileTask(URL remoteUrl, File tempFile) {
- return Futures.submitAsync(
- new FetchToFileTask(remoteUrl, tempFile, null, null, null, 0) {
- @Override
- public ListenableFuture<Void> call() throws Exception {
- numRequests += 1;
- return super.call();
- }
- },
- executorService);
- }
-
- public int getNumRequests() {
- return numRequests;
- }
- }
-
- static void addTestArtifact(DependencyJar dependencyJar) throws IOException {
- MavenJarArtifact mavenJarArtifact = new MavenJarArtifact(dependencyJar);
- try {
- Files.createParentDirs(new File(REPOSITORY_DIR, mavenJarArtifact.jarPath()));
- String jarContents = mavenJarArtifact.toString() + " jar contents";
- Files.write(
- jarContents.getBytes(StandardCharsets.UTF_8),
- new File(REPOSITORY_DIR, mavenJarArtifact.jarPath()));
- Files.write(
- sha512(jarContents).getBytes(),
- new File(REPOSITORY_DIR, mavenJarArtifact.jarSha512Path()));
- String pomContents = mavenJarArtifact.toString() + " pom contents";
- Files.write(
- pomContents.getBytes(StandardCharsets.UTF_8),
- new File(REPOSITORY_DIR, mavenJarArtifact.pomPath()));
- Files.write(
- sha512(pomContents).getBytes(),
- new File(REPOSITORY_DIR, mavenJarArtifact.pomSha512Path()));
- } catch (MalformedURLException e) {
- throw new AssertionError(e);
- }
- }
-
- static void addTestArtifactInvalidSha512(DependencyJar dependencyJar) throws IOException {
- MavenJarArtifact mavenJarArtifact = new MavenJarArtifact(dependencyJar);
- try {
- Files.createParentDirs(new File(REPOSITORY_DIR, mavenJarArtifact.jarPath()));
- String jarContents = mavenJarArtifact.toString() + " jar contents";
- Files.write(jarContents.getBytes(), new File(REPOSITORY_DIR, mavenJarArtifact.jarPath()));
- Files.write(
- sha512("No the same content").getBytes(),
- new File(REPOSITORY_DIR, mavenJarArtifact.jarSha512Path()));
- String pomContents = mavenJarArtifact.toString() + " pom contents";
- Files.write(pomContents.getBytes(), new File(REPOSITORY_DIR, mavenJarArtifact.pomPath()));
- Files.write(
- sha512("Really not the same content").getBytes(),
- new File(REPOSITORY_DIR, mavenJarArtifact.pomSha512Path()));
- } catch (MalformedURLException e) {
- throw new AssertionError(e);
- }
- }
-
- static String sha512(String contents) {
- return SHA512.hashString(contents, StandardCharsets.UTF_8).toString();
- }
-
- static String readFile(File file) throws IOException {
- return new String(Files.asByteSource(file).read(), StandardCharsets.UTF_8);
- }
-}
diff --git a/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.kt b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.kt
new file mode 100644
index 000000000..1f5c3f836
--- /dev/null
+++ b/plugins/maven-dependency-resolver/src/test/java/org/robolectric/internal/dependency/MavenDependencyResolverTest.kt
@@ -0,0 +1,294 @@
+package org.robolectric.internal.dependency
+
+import com.google.common.hash.Hashing
+import com.google.common.io.Files
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.util.concurrent.MoreExecutors
+import java.io.File
+import java.io.IOException
+import java.net.MalformedURLException
+import java.net.URL
+import java.nio.charset.StandardCharsets
+import java.util.concurrent.ExecutorService
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MavenDependencyResolverTest {
+ private lateinit var localRepositoryDir: File
+ private lateinit var executorService: ExecutorService
+ private lateinit var mavenDependencyResolver: MavenDependencyResolver
+ private lateinit var mavenArtifactFetcher: TestMavenArtifactFetcher
+
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ executorService = MoreExecutors.newDirectExecutorService()
+ localRepositoryDir = Files.createTempDir()
+ localRepositoryDir.deleteOnExit()
+ mavenArtifactFetcher =
+ TestMavenArtifactFetcher(
+ REPOSITORY_URL,
+ REPOSITORY_USERNAME,
+ REPOSITORY_PASSWORD,
+ PROXY_HOST,
+ PROXY_PORT,
+ localRepositoryDir,
+ executorService
+ )
+ mavenDependencyResolver = TestMavenDependencyResolver()
+ }
+
+ @Throws(Exception::class)
+ @Test
+ fun localArtifactUrl_placesFilesCorrectlyForSingleURL() {
+ val dependencyJar = successCases[0]
+ mavenDependencyResolver.getLocalArtifactUrl(dependencyJar)
+ assertThat(mavenArtifactFetcher.numRequests).isEqualTo(4)
+ val artifact = MavenJarArtifact(dependencyJar)
+ checkJarArtifact(artifact)
+ }
+
+ @Throws(Exception::class)
+ @Test
+ fun localArtifactUrl_placesFilesCorrectlyForMultipleURL() {
+ mavenDependencyResolver.getLocalArtifactUrls(*successCases)
+ assertThat(mavenArtifactFetcher.numRequests).isEqualTo(4 * successCases.size)
+ for (dependencyJar in successCases) {
+ val artifact = MavenJarArtifact(dependencyJar)
+ checkJarArtifact(artifact)
+ }
+ }
+
+ /** Checks the case where the existing artifact directory is valid. */
+ @Throws(Exception::class)
+ @Test
+ fun localArtifactUrl_handlesExistingArtifactDirectory() {
+ val dependencyJar = DependencyJar("group", "artifact", "1")
+ val mavenJarArtifact = MavenJarArtifact(dependencyJar)
+ val jarFile = File(localRepositoryDir, mavenJarArtifact.jarPath())
+ Files.createParentDirs(jarFile)
+ assertThat(jarFile.parentFile.isDirectory).isTrue()
+ mavenDependencyResolver.getLocalArtifactUrl(dependencyJar)
+ checkJarArtifact(mavenJarArtifact)
+ }
+
+ /**
+ * Checks the case where there is some existing artifact metadata in the artifact directory, but
+ * not the JAR.
+ */
+ @Throws(Exception::class)
+ @Test
+ fun localArtifactUrl_handlesExistingMetadataFile() {
+ val dependencyJar = DependencyJar("group", "artifact", "1")
+ val mavenJarArtifact = MavenJarArtifact(dependencyJar)
+ val pomFile = File(localRepositoryDir, mavenJarArtifact.pomPath())
+ pomFile.parentFile.mkdirs()
+ Files.write(ByteArray(0), pomFile)
+ assertThat(pomFile.exists()).isTrue()
+ mavenDependencyResolver.getLocalArtifactUrl(dependencyJar)
+ checkJarArtifact(mavenJarArtifact)
+ }
+
+ @Throws(Exception::class)
+ private fun checkJarArtifact(artifact: MavenJarArtifact) {
+ val jar = File(localRepositoryDir, artifact.jarPath())
+ val pom = File(localRepositoryDir, artifact.pomPath())
+ val jarSha512 = File(localRepositoryDir, artifact.jarSha512Path())
+ val pomSha512 = File(localRepositoryDir, artifact.pomSha512Path())
+ assertThat(jar.exists()).isTrue()
+ assertThat(readFile(jar)).isEqualTo("$artifact jar contents")
+ assertThat(pom.exists()).isTrue()
+ assertThat(readFile(pom)).isEqualTo("$artifact pom contents")
+ assertThat(jarSha512.exists()).isTrue()
+ assertThat(readFile(jarSha512)).isEqualTo(sha512("$artifact jar contents"))
+ assertThat(pom.exists()).isTrue()
+ assertThat(readFile(pomSha512)).isEqualTo(sha512("$artifact pom contents"))
+ }
+
+ @Throws(Exception::class)
+ @Test
+ fun localArtifactUrl_doesNotFetchWhenArtifactsExist() {
+ val dependencyJar = DependencyJar("group", "artifact", "1")
+ val mavenJarArtifact = MavenJarArtifact(dependencyJar)
+ val artifactFile = File(localRepositoryDir, mavenJarArtifact.jarPath())
+ artifactFile.parentFile.mkdirs()
+ Files.write(ByteArray(0), artifactFile)
+ assertThat(artifactFile.exists()).isTrue()
+ mavenDependencyResolver.getLocalArtifactUrl(dependencyJar)
+ assertThat(mavenArtifactFetcher.numRequests).isEqualTo(0)
+ }
+
+ @Throws(Exception::class)
+ @Test
+ fun localArtifactUrl_handlesFileNotFound() {
+ val dependencyJar = DependencyJar("group", "missing-artifact", "1")
+ Assert.assertThrows(AssertionError::class.java) {
+ mavenDependencyResolver.getLocalArtifactUrl(dependencyJar)
+ }
+ }
+
+ @Throws(Exception::class)
+ @Test
+ fun localArtifactUrl_handlesInvalidSha512() {
+ val dependencyJar = DependencyJar("group", "artifact-invalid-sha512", "1")
+ addTestArtifactInvalidSha512(dependencyJar)
+ Assert.assertThrows(AssertionError::class.java) {
+ mavenDependencyResolver.getLocalArtifactUrl(dependencyJar)
+ }
+ }
+
+ internal inner class TestMavenDependencyResolver : MavenDependencyResolver() {
+ override fun createMavenFetcher(
+ repositoryUrl: String?,
+ repositoryUserName: String?,
+ repositoryPassword: String?,
+ proxyHost: String?,
+ proxyPort: Int,
+ localRepositoryDir: File,
+ executorService: ExecutorService
+ ): MavenArtifactFetcher {
+ return mavenArtifactFetcher
+ }
+
+ override fun createExecutorService(): ExecutorService {
+ return executorService
+ }
+
+ override fun createLockFile(): File {
+ return try {
+ File.createTempFile("MavenDependencyResolverTest", null)
+ } catch (e: IOException) {
+ throw AssertionError(e)
+ }
+ }
+ }
+
+ internal class TestMavenArtifactFetcher(
+ repositoryUrl: String?,
+ repositoryUserName: String?,
+ repositoryPassword: String?,
+ proxyHost: String?,
+ proxyPort: Int,
+ localRepositoryDir: File,
+ private val executorService: ExecutorService
+ ) :
+ MavenArtifactFetcher(
+ repositoryUrl,
+ repositoryUserName,
+ repositoryPassword,
+ proxyHost,
+ proxyPort,
+ localRepositoryDir,
+ executorService
+ ) {
+ var numRequests = 0
+ private set
+
+ override fun createFetchToFileTask(remoteUrl: URL, tempFile: File): ListenableFuture<Void> {
+ return Futures.submitAsync(
+ object : FetchToFileTask(remoteUrl, tempFile, null, null, null, 0) {
+ @Throws(Exception::class)
+ override fun call(): ListenableFuture<Void> {
+ numRequests += 1
+ return super.call()
+ }
+ },
+ executorService
+ )
+ }
+ }
+
+ companion object {
+ private var REPOSITORY_DIR: File
+ private var REPOSITORY_URL: String
+ private const val REPOSITORY_USERNAME = "username"
+ private const val REPOSITORY_PASSWORD = "password"
+ private const val PROXY_HOST = "123.4.5.678"
+ private const val PROXY_PORT = 9000
+ private val SHA512 = Hashing.sha512()
+ private val successCases =
+ arrayOf(
+ DependencyJar("group", "artifact", "1"),
+ DependencyJar("org.group2", "artifact2-name", "2.4.5"),
+ DependencyJar("org.robolectric", "android-all", "10-robolectric-5803371")
+ )
+
+ init {
+ try {
+ REPOSITORY_DIR = Files.createTempDir()
+ REPOSITORY_DIR.deleteOnExit()
+ REPOSITORY_URL = REPOSITORY_DIR.toURI().toURL().toString()
+ for (dependencyJar in successCases) {
+ addTestArtifact(dependencyJar)
+ }
+ } catch (e: Exception) {
+ throw AssertionError(e)
+ }
+ }
+
+ @Throws(IOException::class)
+ fun addTestArtifact(dependencyJar: DependencyJar?) {
+ val mavenJarArtifact = MavenJarArtifact(dependencyJar)
+ try {
+ Files.createParentDirs(File(REPOSITORY_DIR, mavenJarArtifact.jarPath()))
+ val jarContents = "$mavenJarArtifact jar contents"
+ Files.write(
+ jarContents.toByteArray(StandardCharsets.UTF_8),
+ File(REPOSITORY_DIR, mavenJarArtifact.jarPath())
+ )
+ Files.write(
+ sha512(jarContents).toByteArray(),
+ File(REPOSITORY_DIR, mavenJarArtifact.jarSha512Path())
+ )
+ val pomContents = "$mavenJarArtifact pom contents"
+ Files.write(
+ pomContents.toByteArray(StandardCharsets.UTF_8),
+ File(REPOSITORY_DIR, mavenJarArtifact.pomPath())
+ )
+ Files.write(
+ sha512(pomContents).toByteArray(),
+ File(REPOSITORY_DIR, mavenJarArtifact.pomSha512Path())
+ )
+ } catch (e: MalformedURLException) {
+ throw AssertionError(e)
+ }
+ }
+
+ @Throws(IOException::class)
+ fun addTestArtifactInvalidSha512(dependencyJar: DependencyJar?) {
+ val mavenJarArtifact = MavenJarArtifact(dependencyJar)
+ try {
+ Files.createParentDirs(File(REPOSITORY_DIR, mavenJarArtifact.jarPath()))
+ val jarContents = "$mavenJarArtifact jar contents"
+ Files.write(jarContents.toByteArray(), File(REPOSITORY_DIR, mavenJarArtifact.jarPath()))
+ Files.write(
+ sha512("No the same content").toByteArray(),
+ File(REPOSITORY_DIR, mavenJarArtifact.jarSha512Path())
+ )
+ val pomContents = "$mavenJarArtifact pom contents"
+ Files.write(pomContents.toByteArray(), File(REPOSITORY_DIR, mavenJarArtifact.pomPath()))
+ Files.write(
+ sha512("Really not the same content").toByteArray(),
+ File(REPOSITORY_DIR, mavenJarArtifact.pomSha512Path())
+ )
+ } catch (e: MalformedURLException) {
+ throw AssertionError(e)
+ }
+ }
+
+ fun sha512(contents: String): String {
+ return SHA512.hashString(contents, StandardCharsets.UTF_8).toString()
+ }
+
+ @Throws(IOException::class)
+ fun readFile(file: File): String {
+ return String(Files.asByteSource(file).read(), StandardCharsets.UTF_8)
+ }
+ }
+}
diff --git a/processor/build.gradle b/processor/build.gradle
index b082b5ee2..ac14e4282 100644
--- a/processor/build.gradle
+++ b/processor/build.gradle
@@ -39,7 +39,7 @@ dependencies {
api "org.ow2.asm:asm:${asmVersion}"
api "org.ow2.asm:asm-commons:${asmVersion}"
api "com.google.guava:guava:$guavaJREVersion"
- api "com.google.code.gson:gson:2.9.1"
+ api "com.google.code.gson:gson:2.10.1"
implementation 'com.google.auto:auto-common:1.1.2'
def toolsJar = Jvm.current().getToolsJar()
@@ -50,6 +50,6 @@ dependencies {
testImplementation "javax.annotation:jsr250-api:1.0"
testImplementation "junit:junit:${junitVersion}"
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
- testImplementation "com.google.testing.compile:compile-testing:0.19"
+ testImplementation "com.google.testing.compile:compile-testing:0.21.0"
testImplementation "com.google.truth:truth:${truthVersion}"
}
diff --git a/resources/build.gradle b/resources/build.gradle
index 9bc1390b5..129dc20ad 100644
--- a/resources/build.gradle
+++ b/resources/build.gradle
@@ -14,6 +14,6 @@ dependencies {
testImplementation "junit:junit:${junitVersion}"
testImplementation "com.google.truth:truth:${truthVersion}"
- testImplementation "com.google.testing.compile:compile-testing:0.19"
+ testImplementation "com.google.testing.compile:compile-testing:0.21.0"
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
}
diff --git a/resources/src/main/java/org/robolectric/res/android/Chunk.java b/resources/src/main/java/org/robolectric/res/android/Chunk.java
index 9318af9e7..71e19e901 100644
--- a/resources/src/main/java/org/robolectric/res/android/Chunk.java
+++ b/resources/src/main/java/org/robolectric/res/android/Chunk.java
@@ -7,6 +7,8 @@ import static org.robolectric.res.android.Util.isTruthy;
import java.nio.ByteBuffer;
import org.robolectric.res.android.ResourceTypes.ResChunk_header;
import org.robolectric.res.android.ResourceTypes.ResStringPool_header;
+import org.robolectric.res.android.ResourceTypes.ResTableStagedAliasEntry;
+import org.robolectric.res.android.ResourceTypes.ResTableStagedAliasHeader;
import org.robolectric.res.android.ResourceTypes.ResTable_header;
import org.robolectric.res.android.ResourceTypes.ResTable_lib_entry;
import org.robolectric.res.android.ResourceTypes.ResTable_lib_header;
@@ -119,6 +121,23 @@ class Chunk {
}
}
+ public ResTableStagedAliasHeader asResTableStagedAliasHeader() {
+ if (header_size() >= ResTableStagedAliasHeader.SIZEOF) {
+ return new ResTableStagedAliasHeader(device_chunk_.myBuf(), device_chunk_.myOffset());
+ } else {
+ return null;
+ }
+ }
+
+ public ResTableStagedAliasEntry asResTableStagedAliasEntry() {
+ if (data_size() >= ResTableStagedAliasEntry.SIZEOF) {
+ return new ResTableStagedAliasEntry(
+ device_chunk_.myBuf(), device_chunk_.myOffset() + header_size());
+ } else {
+ return null;
+ }
+ }
+
static class Iterator {
private ResChunk_header next_chunk_;
private int len_;
diff --git a/resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java b/resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java
index b590cca62..4bb34f41b 100644
--- a/resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java
+++ b/resources/src/main/java/org/robolectric/res/android/CppAssetManager2.java
@@ -343,6 +343,17 @@ public class CppAssetManager2 {
for (PackageGroup iter2 : package_groups_) {
iter2.dynamic_ref_table.addMapping(package_name,
iter.dynamic_ref_table.mAssignedPackageId);
+
+ // Add the alias resources to the dynamic reference table of every package group. Since
+ // staging aliases can only be defined by the framework package (which is not a shared
+ // library), the compile-time package id of the framework is the same across all packages
+ // that compile against the framework.
+ for (ConfiguredPackage pkg : iter.packages_) {
+ for (Map.Entry<Integer, Integer> entry :
+ pkg.loaded_package_.getAliasResourceIdMap().entrySet()) {
+ iter2.dynamic_ref_table.addAlias(entry.getKey(), entry.getValue());
+ }
+ }
}
}
}
diff --git a/resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java b/resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java
index 4897ec6f4..0604116be 100644
--- a/resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java
+++ b/resources/src/main/java/org/robolectric/res/android/DynamicRefTable.java
@@ -92,14 +92,26 @@ public class DynamicRefTable
return NO_ERROR;
}
+ void addAlias(int stagedId, int finalizedId) {
+ mAliasId.put(stagedId, finalizedId);
+ }
+
// // Performs the actual conversion of build-time resource ID to run-time
// // resource ID.
int lookupResourceId(Ref<Integer> resId) {
int res = resId.get();
int packageId = Res_GETPACKAGE(res) + 1;
- if (packageId == APP_PACKAGE_ID && !mAppAsLib) {
- // No lookup needs to be done, app package IDs are absolute.
+ Integer aliasId = mAliasId.get(res);
+ if (aliasId != null) {
+ // Rewrite the resource id to its alias resource id. Since the alias resource id is a
+ // compile-time id, it still needs to be resolved further.
+ res = aliasId;
+ }
+
+ if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) {
+ // No lookup needs to be done, app and framework package IDs are absolute.
+ resId.set(res);
return NO_ERROR;
}
@@ -179,4 +191,5 @@ public class DynamicRefTable
final byte[] mLookupTable = new byte[256];
final Map<String, Byte> mEntries = new HashMap<>();
boolean mAppAsLib;
+ final Map<Integer, Integer> mAliasId = new HashMap<>();
}
diff --git a/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java b/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
index 349222b72..0de3390e7 100644
--- a/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
+++ b/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
@@ -5,6 +5,7 @@ import static org.robolectric.res.android.Errors.NO_INIT;
import static org.robolectric.res.android.ResourceTypes.RES_STRING_POOL_TYPE;
import static org.robolectric.res.android.ResourceTypes.RES_TABLE_LIBRARY_TYPE;
import static org.robolectric.res.android.ResourceTypes.RES_TABLE_PACKAGE_TYPE;
+import static org.robolectric.res.android.ResourceTypes.RES_TABLE_STAGED_ALIAS_TYPE;
import static org.robolectric.res.android.ResourceTypes.RES_TABLE_TYPE;
import static org.robolectric.res.android.ResourceTypes.RES_TABLE_TYPE_SPEC_TYPE;
import static org.robolectric.res.android.ResourceTypes.RES_TABLE_TYPE_TYPE;
@@ -19,6 +20,7 @@ import static org.robolectric.res.android.Util.logWarning;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -27,6 +29,8 @@ import org.robolectric.res.android.Chunk.Iterator;
import org.robolectric.res.android.Idmap.LoadedIdmap;
import org.robolectric.res.android.ResourceTypes.IdmapEntry_header;
import org.robolectric.res.android.ResourceTypes.ResStringPool_header;
+import org.robolectric.res.android.ResourceTypes.ResTableStagedAliasEntry;
+import org.robolectric.res.android.ResourceTypes.ResTableStagedAliasHeader;
import org.robolectric.res.android.ResourceTypes.ResTable_entry;
import org.robolectric.res.android.ResourceTypes.ResTable_header;
import org.robolectric.res.android.ResourceTypes.ResTable_lib_entry;
@@ -43,22 +47,25 @@ import org.robolectric.res.android.ResourceTypes.Res_value;
// and https://android.googlesource.com/platform/frameworks/base/+/android-9.0.0_r12/libs/androidfw/LoadedArsc.cpp
public class LoadedArsc {
- //#ifndef LOADEDARSC_H_
-//#define LOADEDARSC_H_
-//
-//#include <memory>
-//#include <set>
-//#include <vector>
-//
-//#include "android-base/macros.h"
-//
-//#include "androidfw/ByteBucketArray.h"
-//#include "androidfw/Chunk.h"
-//#include "androidfw/ResourceTypes.h"
-//#include "androidfw/Util.h"
-//
-//namespace android {
-//
+ // #ifndef LOADEDARSC_H_
+ // #define LOADEDARSC_H_
+ //
+ // #include <memory>
+ // #include <set>
+ // #include <vector>
+ //
+ // #include "android-base/macros.h"
+ //
+ // #include "androidfw/ByteBucketArray.h"
+ // #include "androidfw/Chunk.h"
+ // #include "androidfw/ResourceTypes.h"
+ // #include "androidfw/Util.h"
+ //
+ // namespace android {
+ //
+
+ private static final int kFrameworkPackageId = 0x01;
+
static class DynamicPackageEntry {
// public:
@@ -286,8 +293,7 @@ public class LoadedArsc {
// reinterpret_cast<uint8_t*>(type) + entry_offset);
ResTable_entry entry = new ResTable_entry(type.myBuf(), type.myOffset() + entry_offset);
- int entry_size = entry.isCompact() ? 8 : dtohs(entry.size);
- // if (entry_size < sizeof(*entry)) {
+ int entry_size = entry.isCompact() ? ResTable_entry.SIZEOF : dtohs(entry.size);
if (entry_size < ResTable_entry.SIZEOF) {
logError("ResTable_entry size " + entry_size + " at offset " + entry_offset
+ " is too small.");
@@ -300,6 +306,7 @@ public class LoadedArsc {
return false;
}
+ // No further validations apply if the entry is compact.
if (entry.isCompact()) {
return true;
}
@@ -368,6 +375,7 @@ public class LoadedArsc {
// };
final Map<Integer, TypeSpec> type_specs_ = new HashMap<>();
final List<DynamicPackageEntry> dynamic_package_map_ = new ArrayList<>();
+ final Map<Integer, Integer> aliasIdMap = new HashMap<>();
ResTable_entry GetEntry(ResTable_type type_chunk,
short entry_index) {
@@ -754,6 +762,68 @@ public class LoadedArsc {
} break;
+ case RES_TABLE_STAGED_ALIAS_TYPE:
+ {
+ if (loaded_package.package_id_ != kFrameworkPackageId) {
+ logWarning(
+ String.format(
+ "Alias chunk ignored for non-framework package '%s'",
+ loaded_package.package_name_));
+ break;
+ }
+
+ ResTableStagedAliasHeader libAlias = child_chunk.asResTableStagedAliasHeader();
+ if (libAlias == null) {
+ logError("RES_TABLE_STAGED_ALIAS_TYPE is too small.");
+ return emptyBraces();
+ }
+ if ((child_chunk.data_size() / ResTableStagedAliasEntry.SIZEOF)
+ < dtohl(libAlias.count)) {
+ logError("RES_TABLE_STAGED_ALIAS_TYPE is too small to hold entries.");
+ return emptyBraces();
+ }
+
+ // const auto entryBegin =
+ // child_chunk.data_ptr().convert<ResTableStagedAliasEntry>();
+ // const auto entry_end = entryBegin + dtohl(libAlias.count);
+ ResTableStagedAliasEntry entryBegin = child_chunk.asResTableStagedAliasEntry();
+ int entryEndOffset =
+ entryBegin.myOffset()
+ + dtohl(libAlias.count) * ResTableStagedAliasEntry.SIZEOF;
+ // std::unordered_set<uint32_t> finalizedIds;
+ // finalizedIds.reserve(entry_end - entryBegin);
+ Set<Integer> finalizedIds = new HashSet<>();
+ for (ResTableStagedAliasEntry entryIter = entryBegin;
+ entryIter.myOffset() != entryEndOffset;
+ entryIter =
+ new ResTableStagedAliasEntry(
+ entryIter.myBuf(),
+ entryIter.myOffset() + ResTableStagedAliasEntry.SIZEOF)) {
+
+ int finalizedId = dtohl(entryIter.finalizedResId);
+ // if (!finalizedIds.insert(finalizedId).second) {
+ if (!finalizedIds.add(finalizedId)) {
+ logError(
+ String.format(
+ "Repeated finalized resource id '%08x' in staged aliases.",
+ finalizedId));
+ return emptyBraces();
+ }
+
+ int stagedId = dtohl(entryIter.stagedResId);
+ // auto [_, success] = loaded_package->aliasIdMap.emplace(stagedId,
+ // finalizedId);
+ Integer previousValue = loaded_package.aliasIdMap.put(stagedId, finalizedId);
+ if (previousValue != null) {
+ logError(
+ String.format(
+ "Repeated staged resource id '%08x' in staged aliases.", stagedId));
+ return emptyBraces();
+ }
+ }
+ }
+ break;
+
default:
logWarning(String.format("Unknown chunk type '%02x'.", chunk.type()));
break;
@@ -857,6 +927,10 @@ public class LoadedArsc {
}
}
+ Map<Integer, Integer> getAliasResourceIdMap() {
+ return aliasIdMap;
+ }
+
private static LoadedPackage emptyBraces() {
return new LoadedPackage();
}
diff --git a/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java b/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java
index 06c1d8f1d..17c9dd91c 100644
--- a/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java
+++ b/resources/src/main/java/org/robolectric/res/android/ResourceTypes.java
@@ -161,6 +161,7 @@ public class ResourceTypes {
public static final int RES_TABLE_TYPE_TYPE = 0x0201;
public static final int RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
public static final int RES_TABLE_LIBRARY_TYPE = 0x0203;
+ public static final int RES_TABLE_STAGED_ALIAS_TYPE = 0x0206;
/**
* Macros for building/splitting resource identifiers.
@@ -1218,6 +1219,7 @@ public static class ResTable_ref
if (off16 == -1) {
return -1;
}
+ // Check for no entry (0xffff short)
return dtohs(off16) == 0xffff ? ResTable_type.NO_ENTRY : dtohs(off16) * 4;
} else {
return byteBuffer.getInt(offset + header.headerSize + entryIndex * 4);
@@ -1296,7 +1298,6 @@ public static class ResTable_ref
// Number of bytes in this structure.
short size;
- //enum {
// If set, this is a complex entry, holding a set of name/value
// mappings. It is followed by an array of ResTable_map structures.
public static final int FLAG_COMPLEX = 0x0001;
@@ -1307,9 +1308,10 @@ public static class ResTable_ref
// resources of the same name/type. This is only useful during
// linking with other resource tables.
public static final int FLAG_WEAK = 0x0004;
-
+ // If set, this is a compact entry with data type and value directly
+ // encoded in the this entry, see ResTable_entry::compact
public static final int FLAG_COMPACT = 0x0008;
- // };
+
final short flags;
// Reference into ResTable_package::keyStrings identifying this entry.
@@ -1332,10 +1334,6 @@ public static class ResTable_ref
}
}
- public boolean isCompact() {
- return (flags & FLAG_COMPACT) == FLAG_COMPACT;
- }
-
public int getKeyIndex() {
if (isCompact()) {
return dtohs(compactKey);
@@ -1344,6 +1342,10 @@ public static class ResTable_ref
}
}
+ public boolean isCompact() {
+ return (flags & FLAG_COMPACT) == FLAG_COMPACT;
+ }
+
public Res_value getResValue() {
// something like:
@@ -1351,10 +1353,10 @@ public static class ResTable_ref
// reinterpret_cast<final byte*>(entry) + dtohs(entry.size));
if (isCompact()) {
- byte type = (byte)(dtohs(flags) >> 8);
+ byte type = (byte) (dtohs(flags) >> 8);
return new Res_value((byte)(dtohs(flags) >> 8), compactData);
} else {
- return new Res_value(myBuf(), myOffset() + dtohs(size));
+ return new Res_value(myBuf(), myOffset() + dtohs(size));
}
}
}
@@ -1544,6 +1546,44 @@ public static class ResTable_ref
}
};
+ /**
+ * A map that allows rewriting staged (non-finalized) resource ids to their finalized
+ * counterparts.
+ */
+ static class ResTableStagedAliasHeader extends WithOffset {
+ public static final int SIZEOF = ResChunk_header.SIZEOF + 4;
+
+ ResChunk_header header;
+
+ // The number of ResTableStagedAliasEntry that follow this header.
+ int count;
+
+ ResTableStagedAliasHeader(ByteBuffer buf, int offset) {
+ super(buf, offset);
+
+ header = new ResChunk_header(buf, offset);
+ count = buf.getInt(offset + ResChunk_header.SIZEOF);
+ }
+ }
+
+ /** Maps the staged (non-finalized) resource id to its finalized resource id. */
+ static class ResTableStagedAliasEntry extends WithOffset {
+ public static final int SIZEOF = 8;
+
+ // The compile-time staged resource id to rewrite.
+ int stagedResId;
+
+ // The compile-time finalized resource id to which the staged resource id should be rewritten.
+ int finalizedResId;
+
+ ResTableStagedAliasEntry(ByteBuffer buf, int offset) {
+ super(buf, offset);
+
+ stagedResId = buf.getInt(offset);
+ finalizedResId = buf.getInt(offset + 4);
+ }
+ }
+
// struct alignas(uint32_t) Idmap_header {
static class Idmap_header extends WithOffset {
// Always 0x504D4449 ('IDMP')
diff --git a/resources/src/test/java/org/robolectric/res/android/LoadedArscTest.java b/resources/src/test/java/org/robolectric/res/android/LoadedArscTest.java
new file mode 100644
index 000000000..73fedeaef
--- /dev/null
+++ b/resources/src/test/java/org/robolectric/res/android/LoadedArscTest.java
@@ -0,0 +1,63 @@
+package org.robolectric.res.android;
+
+import static org.junit.Assert.assertEquals;
+import static org.robolectric.res.android.ResourceTypes.RES_TABLE_PACKAGE_TYPE;
+import static org.robolectric.res.android.ResourceTypes.RES_TABLE_STAGED_ALIAS_TYPE;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.robolectric.res.android.ResourceTypes.ResChunk_header;
+
+@RunWith(JUnit4.class)
+public class LoadedArscTest {
+
+ @Test
+ public void testResTableStagedAliasType() {
+ final int stagedResId = 0x123;
+ final int finalizedResId = 0x456;
+
+ final ByteBuffer buf = ByteBuffer.allocate(1024);
+
+ ResChunk_header.write(
+ buf,
+ (short) RES_TABLE_PACKAGE_TYPE,
+ () -> {
+ // header
+ buf.putInt(0x01); // ResTable_package.id
+ // ResTable_package.name
+ for (int i = 0; i < 128; i++) {
+ buf.putChar('\0');
+ }
+ buf.putInt(0); // ResTable_package.typeStrings
+ buf.putInt(0); // ResTable_package.lastPublicType
+ buf.putInt(0); // ResTable_package.keyStrings
+ buf.putInt(0); // ResTable_package.lastPublicKey
+ buf.putInt(0); // ResTable_package.typeIdOffset
+ },
+ () -> {
+ // contents
+ ResChunk_header.write(
+ buf,
+ (short) RES_TABLE_STAGED_ALIAS_TYPE,
+ () -> {
+ // header
+ buf.putInt(1); // ResTableStagedAliasHeader.count
+ },
+ () -> {
+ // contents
+ buf.putInt(stagedResId); // ResTableStagedAliasEntry.stagedResId
+ buf.putInt(finalizedResId); // ResTableStagedAliasEntry.finalizedResId
+ });
+ });
+ final Chunk chunk = new Chunk(new ResChunk_header(buf, 0));
+ final LoadedArsc.LoadedPackage loadedPackage =
+ LoadedArsc.LoadedPackage.Load(
+ chunk, null /* loaded_idmap */, true /* system */, false /* load_as_shared_library */);
+
+ final Map<Integer, Integer> aliasIdMap = loadedPackage.getAliasResourceIdMap();
+ assertEquals(finalizedResId, (int) aliasIdMap.get(stagedResId));
+ }
+}
diff --git a/robolectric/build.gradle b/robolectric/build.gradle
index cf3f7e3d5..faaa8b3a0 100644
--- a/robolectric/build.gradle
+++ b/robolectric/build.gradle
@@ -4,20 +4,6 @@ import org.robolectric.gradle.RoboJavaModulePlugin
apply plugin: RoboJavaModulePlugin
apply plugin: DeployedRoboJavaModulePlugin
-processResources {
- filesMatching("**/robolectric-version.properties") {
- filter { String line ->
- return line.replaceAll(/\$\{project.version\}/, project.version)
- }
- }
-}
-
-configurations {
- shadow
-}
-
-project.sourceSets.test.compileClasspath += configurations.shadow
-
dependencies {
annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
@@ -39,7 +25,7 @@ dependencies {
api project(":shadows:framework")
implementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2'
- api "org.bouncycastle:bcprov-jdk15on:1.70"
+ api "org.bouncycastle:bcprov-jdk18on:1.72"
compileOnly "com.google.code.findbugs:jsr305:3.0.2"
compileOnly AndroidSdk.MAX_SDK.coordinates
@@ -61,13 +47,6 @@ dependencies {
testRuntimeOnly AndroidSdk.MAX_SDK.coordinates // run against whatever this JDK supports
}
-test {
- if (project.hasProperty('maxParallelForks'))
- maxParallelForks = project.maxParallelForks as int
- if (project.hasProperty('forkEvery'))
- forkEvery = project.forkEvery as int
-}
-
project.apply plugin: CheckApiChangesPlugin
checkApiChanges {
diff --git a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
index affe32afa..519f42c09 100644
--- a/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
+++ b/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java
@@ -24,6 +24,7 @@ import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.robolectric.android.AndroidSdkShadowMatcher;
import org.robolectric.annotation.Config;
+import org.robolectric.annotation.GraphicsMode;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.annotation.SQLiteMode;
@@ -289,9 +290,14 @@ public class RobolectricTestRunner extends SandboxTestRunner {
? SQLiteMode.Mode.LEGACY
: roboMethod.configuration.get(SQLiteMode.Mode.class);
+ GraphicsMode.Mode graphicsMode =
+ roboMethod.configuration == null
+ ? GraphicsMode.Mode.LEGACY
+ : roboMethod.configuration.get(GraphicsMode.Mode.class);
+
sdk.verifySupportedSdk(method.getDeclaringClass().getName());
return sandboxManager.getAndroidSandbox(
- classLoaderConfig, sdk, resourcesMode, looperMode, sqliteMode);
+ classLoaderConfig, sdk, resourcesMode, looperMode, sqliteMode, graphicsMode);
}
@Override
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
index cdcac2bd1..a807d4e68 100755
--- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
@@ -176,6 +176,8 @@ public class AndroidTestEnvironment implements TestEnvironment {
Bootstrap.applyQualifiers(config.qualifiers(), apiLevel, androidConfiguration, displayMetrics);
+ androidConfiguration.fontScale = config.fontScale();
+
if (Boolean.getBoolean("robolectric.nativeruntime.enableGraphics")) {
Bitmap.setDefaultDensity(displayMetrics.densityDpi);
}
@@ -697,8 +699,7 @@ public class AndroidTestEnvironment implements TestEnvironment {
for (String action : receiver.getActions()) {
filter.addAction(action);
}
- String receiverClassName = replaceLastDotWith$IfInnerStaticClass(receiver.getName());
- application.registerReceiver((BroadcastReceiver) newInstanceOf(receiverClassName), filter);
+ application.registerReceiver((BroadcastReceiver) newInstanceOf(receiver.getName()), filter);
}
}
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
index 08fcffc9d..a6908af09 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
@@ -177,7 +177,6 @@ public class LocalUiController implements UiController {
Set<IdlingResourceProxy> activeResources = new HashSet<>();
long startTimeNanos = System.nanoTime();
- shadowMainLooper.idle();
while (true) {
// Gather the list of resources that are not idling.
for (IdlingResourceProxy resource : idlingResources) {
@@ -196,13 +195,13 @@ public class LocalUiController implements UiController {
});
}
// If all are idle then just return, we're done.
- if (activeResources.isEmpty()) {
+ if (activeResources.isEmpty() && shadowMainLooper.isIdle()) {
break;
}
// While the resources that weren't idle haven't transitioned to idle continue to loop the
// main looper waiting for any new messages. Once all resources have transitioned to idle loop
// around again to make sure all resources are idle at the same time.
- while (!activeResources.isEmpty()) {
+ do {
long elapsedTimeMs = NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos);
if (elapsedTimeMs >= idlingResourceErrorTimeoutMs) {
throw new IdlingResourceTimeoutException(idlingResourceNames(activeResources));
@@ -210,7 +209,7 @@ public class LocalUiController implements UiController {
// Poll the queue and suspend the thread until we get new messages or the idle transition.
shadowMainLooper.poll(idlingResourceErrorTimeoutMs - elapsedTimeMs);
shadowMainLooper.idle();
- }
+ } while (!activeResources.isEmpty());
}
}
diff --git a/robolectric/src/main/java/org/robolectric/internal/SandboxManager.java b/robolectric/src/main/java/org/robolectric/internal/SandboxManager.java
index 73dfae3e7..23944f8cf 100644
--- a/robolectric/src/main/java/org/robolectric/internal/SandboxManager.java
+++ b/robolectric/src/main/java/org/robolectric/internal/SandboxManager.java
@@ -6,6 +6,7 @@ import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
+import org.robolectric.annotation.GraphicsMode;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.SQLiteMode;
import org.robolectric.internal.bytecode.InstrumentationConfiguration;
@@ -49,8 +50,10 @@ public class SandboxManager {
Sdk sdk,
ResourcesMode resourcesMode,
LooperMode.Mode looperMode,
- SQLiteMode.Mode sqliteMode) {
- SandboxKey key = new SandboxKey(instrumentationConfig, sdk, resourcesMode, looperMode);
+ SQLiteMode.Mode sqliteMode,
+ GraphicsMode.Mode graphicsMode) {
+ SandboxKey key =
+ new SandboxKey(instrumentationConfig, sdk, resourcesMode, looperMode, graphicsMode);
AndroidSandbox androidSandbox = sandboxesByKey.get(key);
if (androidSandbox == null) {
@@ -79,16 +82,19 @@ public class SandboxManager {
private final InstrumentationConfiguration instrumentationConfiguration;
private final ResourcesMode resourcesMode;
private final LooperMode.Mode looperMode;
+ private final GraphicsMode.Mode graphicsMode;
public SandboxKey(
InstrumentationConfiguration instrumentationConfiguration,
Sdk sdk,
ResourcesMode resourcesMode,
- LooperMode.Mode looperMode) {
+ LooperMode.Mode looperMode,
+ GraphicsMode.Mode graphicsMode) {
this.sdk = sdk;
this.instrumentationConfiguration = instrumentationConfiguration;
this.resourcesMode = resourcesMode;
this.looperMode = looperMode;
+ this.graphicsMode = graphicsMode;
}
@Override
@@ -103,12 +109,14 @@ public class SandboxManager {
return resourcesMode == that.resourcesMode
&& Objects.equals(sdk, that.sdk)
&& Objects.equals(instrumentationConfiguration, that.instrumentationConfiguration)
- && looperMode == that.looperMode;
+ && looperMode == that.looperMode
+ && graphicsMode == that.graphicsMode;
}
@Override
public int hashCode() {
- return Objects.hash(sdk, instrumentationConfiguration, resourcesMode, looperMode);
+ return Objects.hash(
+ sdk, instrumentationConfiguration, resourcesMode, looperMode, graphicsMode);
}
}
}
diff --git a/robolectric/src/main/resources/robolectric-version.properties b/robolectric/src/main/resources/robolectric-version.properties
deleted file mode 100644
index 6c47e07bb..000000000
--- a/robolectric/src/main/resources/robolectric-version.properties
+++ /dev/null
@@ -1 +0,0 @@
-robolectric.version=${project.version} \ No newline at end of file
diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
index 2d8f35823..0ba55d893 100644
--- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
+++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
@@ -160,14 +160,15 @@ public class RobolectricTestRunnerTest {
new TestEnvironmentSpec(AndroidTestEnvironmentWithFailingSetUp.class))
.build());
runner.run(notifier);
- assertThat(events).containsExactly(
- "started: first",
- "failure: fake error in setUpApplicationState",
- "finished: first",
- "started: second",
- "failure: fake error in setUpApplicationState",
- "finished: second"
- ).inOrder();
+ assertThat(events)
+ .containsExactly(
+ "started: first",
+ "failure: ShadowActivityThread.reset: ActivityThread not set",
+ "finished: first",
+ "started: second",
+ "failure: ShadowActivityThread.reset: ActivityThread not set",
+ "finished: second")
+ .inOrder();
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java b/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java
index a438f4d86..7c73aaf4a 100644
--- a/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/controller/ActivityControllerTest.java
@@ -363,6 +363,38 @@ public class ActivityControllerTest {
}
@Test
+ public void isChangingConfiguration() {
+ try (ActivityController<ConfigChangeActivity> controller =
+ Robolectric.buildActivity(ConfigChangeActivity.class)) {
+
+ controller.recreate();
+
+ assertThat(transcript).containsExactly("onPause true", "onStop true", "onDestroy true");
+ }
+ }
+
+ private static class ConfigChangeActivity extends Activity {
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ transcript.add("onPause " + isChangingConfigurations());
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ transcript.add("onStop " + isChangingConfigurations());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ transcript.add("onDestroy " + isChangingConfigurations());
+ }
+ }
+
+ @Test
public void windowFocusChanged() {
controller.setup();
assertThat(transcript).doesNotContain("finishedOnWindowFocusChanged");
diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
index ddc3accf8..051188139 100644
--- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
@@ -9,6 +9,7 @@ import static org.robolectric.annotation.ConscryptMode.Mode.ON;
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
import android.app.Application;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -193,7 +194,7 @@ public class AndroidTestEnvironmentTest {
}
@Test
- public void setUpApplicationState_shouldCreateStorageDirs() throws Exception {
+ public void setUpApplicationState_shouldCreateStorageDirs() {
bootstrapWrapper.callSetUpApplicationState();
ApplicationInfo applicationInfo = ApplicationProvider.getApplicationContext()
.getApplicationInfo();
@@ -210,7 +211,7 @@ public class AndroidTestEnvironmentTest {
@Test
@Config(minSdk = Build.VERSION_CODES.N)
- public void setUpApplicationState_shouldCreateStorageDirs_Nplus() throws Exception {
+ public void setUpApplicationState_shouldCreateStorageDirs_Nplus() {
bootstrapWrapper.callSetUpApplicationState();
ApplicationInfo applicationInfo = ApplicationProvider.getApplicationContext()
.getApplicationInfo();
@@ -269,24 +270,27 @@ public class AndroidTestEnvironmentTest {
}
}
- @Test @Config(qualifiers = "b+fr+Cyrl+UK")
- public void localeIsSet() throws Exception {
+ @Test
+ @Config(qualifiers = "b+fr+Cyrl+UK")
+ public void localeIsSet() {
bootstrapWrapper.callSetUpApplicationState();
assertThat(Locale.getDefault().getLanguage()).isEqualTo("fr");
assertThat(Locale.getDefault().getScript()).isEqualTo("Cyrl");
assertThat(Locale.getDefault().getCountry()).isEqualTo("UK");
}
- @Test @Config(qualifiers = "w123dp-h456dp")
- public void whenNotPrefixedWithPlus_setQualifiers_shouldNotBeBasedOnPreviousConfig() throws Exception {
+ @Test
+ @Config(qualifiers = "w123dp-h456dp")
+ public void whenNotPrefixedWithPlus_setQualifiers_shouldNotBeBasedOnPreviousConfig() {
bootstrapWrapper.callSetUpApplicationState();
RuntimeEnvironment.setQualifiers("land");
assertThat(RuntimeEnvironment.getQualifiers()).contains("w470dp-h320dp");
assertThat(RuntimeEnvironment.getQualifiers()).contains("-land-");
}
- @Test @Config(qualifiers = "w100dp-h125dp")
- public void whenDimensAndSizeSpecified_setQualifiers_should() throws Exception {
+ @Test
+ @Config(qualifiers = "w100dp-h125dp")
+ public void whenDimensAndSizeSpecified_setQualifiers_should() {
bootstrapWrapper.callSetUpApplicationState();
RuntimeEnvironment.setQualifiers("+xlarge");
Configuration configuration = Resources.getSystem().getConfiguration();
@@ -295,13 +299,35 @@ public class AndroidTestEnvironmentTest {
assertThat(DeviceConfig.getScreenSize(configuration)).isEqualTo(ScreenSize.xlarge);
}
- @Test @Config(qualifiers = "w123dp-h456dp")
- public void whenPrefixedWithPlus_setQualifiers_shouldBeBasedOnPreviousConfig() throws Exception {
+ @Test
+ @Config(qualifiers = "w123dp-h456dp")
+ public void whenPrefixedWithPlus_setQualifiers_shouldBeBasedOnPreviousConfig() {
bootstrapWrapper.callSetUpApplicationState();
RuntimeEnvironment.setQualifiers("+w124dp");
assertThat(RuntimeEnvironment.getQualifiers()).contains("w124dp-h456dp");
}
+ @Test
+ @Config(fontScale = 1.3f)
+ public void setFontScale_updatesFontScale() {
+ bootstrapWrapper.callSetUpApplicationState();
+
+ Context context = ApplicationProvider.getApplicationContext();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ assertThat(context.getResources().getConfiguration().fontScale).isEqualTo(1.3f);
+ assertThat(displayMetrics.scaledDensity).isEqualTo(displayMetrics.density * 1.3f);
+ }
+
+ @Test
+ public void fontScaleNotSet_stillSetToDefault() {
+ bootstrapWrapper.callSetUpApplicationState();
+
+ Context context = ApplicationProvider.getApplicationContext();
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ assertThat(context.getResources().getConfiguration().fontScale).isEqualTo(1.0f);
+ assertThat(displayMetrics.scaledDensity).isEqualTo(displayMetrics.density);
+ }
+
@LazyApplication(LazyLoad.ON)
@Test
public void resetState_doesNotLoadApplication() {
diff --git a/robolectric/src/test/java/org/robolectric/plugins/GraphicsModeTransitionTest.java b/robolectric/src/test/java/org/robolectric/plugins/GraphicsModeTransitionTest.java
new file mode 100644
index 000000000..5f3223d02
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/plugins/GraphicsModeTransitionTest.java
@@ -0,0 +1,52 @@
+package org.robolectric.plugins;
+
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.annotation.GraphicsMode.Mode.LEGACY;
+import static org.robolectric.annotation.GraphicsMode.Mode.NATIVE;
+
+import android.graphics.Matrix;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.GraphicsMode;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowLegacyMatrix;
+import org.robolectric.shadows.ShadowMatrix;
+import org.robolectric.shadows.ShadowNativeMatrix;
+
+/**
+ * Tests methods that cause transitions to different graphics modes. This is to verify that shadow
+ * invalidation of graphics shadows occurs when the graphics mode changes.
+ *
+ * <p>Method order is important to ensure consistent transitions between LEGACY -> NATIVE -> LEGACY
+ * graphics.
+ */
+@RunWith(AndroidJUnit4.class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Config(sdk = TIRAMISU)
+public class GraphicsModeTransitionTest {
+ @GraphicsMode(LEGACY)
+ @Test
+ public void test1Legacy() {
+ ShadowMatrix shadowMatrix = Shadow.extract(new Matrix());
+ assertThat(shadowMatrix).isInstanceOf(ShadowLegacyMatrix.class);
+ }
+
+ @GraphicsMode(NATIVE)
+ @Test
+ public void test2NativeAfterLegacy() {
+ ShadowMatrix shadowMatrix = Shadow.extract(new Matrix());
+ assertThat(shadowMatrix).isInstanceOf(ShadowNativeMatrix.class);
+ }
+
+ @GraphicsMode(LEGACY)
+ @Test
+ public void test3LegacyAfterNative() {
+ ShadowMatrix shadowMatrix = Shadow.extract(new Matrix());
+ assertThat(shadowMatrix).isInstanceOf(ShadowLegacyMatrix.class);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/AudioDeviceInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/AudioDeviceInfoBuilderTest.java
new file mode 100644
index 000000000..e1961877a
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/AudioDeviceInfoBuilderTest.java
@@ -0,0 +1,25 @@
+package org.robolectric.shadows;
+
+import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP;
+import static android.os.Build.VERSION_CODES.M;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.AudioDeviceInfo;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link AudioDeviceInfoBuilder}. */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = M)
+public class AudioDeviceInfoBuilderTest {
+
+ @Test
+ public void canCreateAudioDeviceInfoWithDesiredType() {
+ AudioDeviceInfo audioDeviceInfo =
+ AudioDeviceInfoBuilder.newBuilder().setType(TYPE_BLUETOOTH_A2DP).build();
+
+ assertThat(audioDeviceInfo.getType()).isEqualTo(TYPE_BLUETOOTH_A2DP);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java
index 10f1fdb92..e718ab2fb 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowClipboardManagerTest.java
@@ -1,6 +1,7 @@
package org.robolectric.shadows;
import static android.content.ClipboardManager.OnPrimaryClipChangedListener;
+import static android.os.Build.VERSION_CODES.P;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -14,6 +15,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
public class ShadowClipboardManagerTest {
@@ -104,4 +106,24 @@ public class ShadowClipboardManagerTest {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, "BLARG?"));
verifyNoMoreInteractions(listener);
}
+
+ @Test
+ @Config(minSdk = P)
+ public void shouldClearPrimaryClip() {
+ clipboardManager.setPrimaryClip(ClipData.newPlainText(null, "BLARG?"));
+ clipboardManager.clearPrimaryClip();
+
+ assertThat(clipboardManager.hasText()).isFalse();
+ assertThat(clipboardManager.hasPrimaryClip()).isFalse();
+ }
+
+ @Test
+ @Config(minSdk = P)
+ public void shouldClearPrimaryClipAndFireListeners() {
+ OnPrimaryClipChangedListener listener = mock(OnPrimaryClipChangedListener.class);
+ clipboardManager.addPrimaryClipChangedListener(listener);
+ clipboardManager.clearPrimaryClip();
+
+ verify(listener).onPrimaryClipChanged();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java
index dd8da17b1..b224e4dfb 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowConnectivityManagerTest.java
@@ -10,12 +10,15 @@ import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
+import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
@@ -321,6 +324,10 @@ public class ShadowConnectivityManagerTest {
};
}
+ private static PendingIntent createSimplePendingIntent() {
+ return PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), 0);
+ }
+
@Test
@Config(minSdk = LOLLIPOP)
public void requestNetwork_shouldAddCallback() throws Exception {
@@ -339,6 +346,15 @@ public class ShadowConnectivityManagerTest {
}
@Test
+ @Config(minSdk = M)
+ public void registerCallback_withPendingIntent_shouldAddCallback() throws Exception {
+ NetworkRequest.Builder builder = new NetworkRequest.Builder();
+ PendingIntent pendingIntent = createSimplePendingIntent();
+ connectivityManager.registerNetworkCallback(builder.build(), pendingIntent);
+ assertThat(shadowOf(connectivityManager).getNetworkCallbackPendingIntents()).hasSize(1);
+ }
+
+ @Test
@Config(minSdk = O)
public void requestNetwork_withTimeout_shouldAddCallback() throws Exception {
NetworkRequest.Builder builder = new NetworkRequest.Builder();
@@ -389,6 +405,21 @@ public class ShadowConnectivityManagerTest {
assertThat(shadowOf(connectivityManager).getNetworkCallbacks()).isEmpty();
}
+ @Test
+ @Config(minSdk = M)
+ public void unregisterCallback_withPendingIntent_shouldRemoveCallbacks() throws Exception {
+ NetworkRequest.Builder builder = new NetworkRequest.Builder();
+ // Add two pendingIntents, should treat them as equal based on Intent#filterEquals
+ PendingIntent pendingIntent1 = createSimplePendingIntent();
+ PendingIntent pendingIntent2 = createSimplePendingIntent();
+ connectivityManager.registerNetworkCallback(builder.build(), pendingIntent1);
+ connectivityManager.registerNetworkCallback(builder.build(), pendingIntent2);
+
+ assertThat(shadowOf(connectivityManager).getNetworkCallbackPendingIntents()).hasSize(1);
+ connectivityManager.unregisterNetworkCallback(pendingIntent2);
+ assertThat(shadowOf(connectivityManager).getNetworkCallbackPendingIntents()).isEmpty();
+ }
+
@Test(expected=IllegalArgumentException.class) @Config(minSdk = LOLLIPOP)
public void unregisterCallback_shouldNotAllowNullCallback() throws Exception {
// Verify that exception is thrown.
@@ -396,6 +427,15 @@ public class ShadowConnectivityManagerTest {
}
@Test
+ @Config(minSdk = M)
+ public void unregisterCallback_withPendingIntent_shouldNotAllowNullCallback() throws Exception {
+ // Verify that exception is thrown.
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> connectivityManager.unregisterNetworkCallback((PendingIntent) null));
+ }
+
+ @Test
public void isActiveNetworkMetered_defaultsToTrue() {
assertThat(connectivityManager.isActiveNetworkMetered()).isTrue();
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java
index c77bb1e14..2c9452c96 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContentResolverTest.java
@@ -1025,7 +1025,12 @@ public class ShadowContentResolverTest {
// unfortunately, there is no direct way of testing if authority is set or not
// however, it's checked in ContentProvider.Transport method calls (validateIncomingUri), so
// it's the closest we can test against
- provider.getIContentProvider().getType(uri); // should not throw
+ if (RuntimeEnvironment.getApiLevel() <= 28) {
+ provider.getIContentProvider().getType(uri); // should not throw
+ } else {
+ // just call validateIncomingUri directly
+ provider.validateIncomingUri(uri);
+ }
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowGeocoderTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowGeocoderTest.java
index d925a61d9..10cddde02 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowGeocoderTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowGeocoderTest.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.robolectric.Shadows.shadowOf;
@@ -7,6 +8,7 @@ import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.location.Address;
import android.location.Geocoder;
+import android.location.Geocoder.GeocodeListener;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
@@ -16,12 +18,14 @@ import java.util.Locale;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
/** Unit test for {@link ShadowGeocoder}. */
@RunWith(AndroidJUnit4.class)
public class ShadowGeocoderTest {
private Geocoder geocoder;
+ private List<Address> decodedAddresses;
@Before
public void setUp() throws Exception {
@@ -64,6 +68,27 @@ public class ShadowGeocoderTest {
}
@Test
+ @Config(minSdk = TIRAMISU)
+ public void getFromLocationSetsListenerWithTheOverwrittenListLimitingByMaxResults() {
+ ShadowGeocoder shadowGeocoder = shadowOf(geocoder);
+
+ List<Address> list =
+ Arrays.asList(new Address(Locale.getDefault()), new Address(Locale.CANADA));
+ shadowGeocoder.setFromLocation(list);
+
+ GeocodeListener geocodeListener = addresses -> decodedAddresses = addresses;
+
+ geocoder.getFromLocation(90.0, 90.0, 1, geocodeListener);
+ assertThat(decodedAddresses).containsExactly(list.get(0));
+
+ geocoder.getFromLocation(90.0, 90.0, 2, geocodeListener);
+ assertThat(decodedAddresses).containsExactly(list.get(0), list.get(1)).inOrder();
+
+ geocoder.getFromLocation(90.0, 90.0, 3, geocodeListener);
+ assertThat(decodedAddresses).containsExactly(list.get(0), list.get(1)).inOrder();
+ }
+
+ @Test
public void getFromLocation_throwsExceptionForInvalidLatitude() throws IOException {
try {
geocoder.getFromLocation(91.0, 90.0, 1);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowHardwareBufferTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowHardwareBufferTest.java
index 35d16b692..fcede3f4e 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowHardwareBufferTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowHardwareBufferTest.java
@@ -2,6 +2,7 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -86,7 +87,7 @@ public class ShadowHardwareBufferTest {
}
@Test
- @Config(minSdk = O)
+ @Config(minSdk = O, maxSdk = TIRAMISU /* framework no longer validates format in > T */)
public void createInvalidFormatThrows() {
try {
HardwareBuffer.create(
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyLooperTest.java
index eda3b1381..88fd2578e 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyLooperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLegacyLooperTest.java
@@ -267,7 +267,7 @@ public class ShadowLegacyLooperTest {
}
@Test
- public void resetThreadLoopers_fromNonMainThread_shouldThrowISE() throws InterruptedException {
+ public void resetThreadLoopers_fromNonMainThread_doesNotThrow() throws InterruptedException {
final AtomicReference<Throwable> ex = new AtomicReference<>();
Thread t =
new Thread() {
@@ -282,7 +282,7 @@ public class ShadowLegacyLooperTest {
};
t.start();
t.join();
- assertThat(ex.get()).isInstanceOf(IllegalStateException.class);
+ assertThat(ex.get()).isNull();
}
@Test
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLinuxTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLinuxTest.java
index bea5e63f5..366daf956 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLinuxTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLinuxTest.java
@@ -2,21 +2,29 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.O;
import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
import android.system.StructStat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.time.Duration;
+import java.util.Arrays;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
-/** Unit tests for ShadowLinux to check values returned from stat() call. */
+/** Unit tests for {@code ShadowLinux}. */
@RunWith(AndroidJUnit4.class)
@Config(minSdk = O)
public final class ShadowLinuxTest {
+ @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
private File file;
private String path;
private ShadowLinux shadowLinux;
@@ -24,10 +32,10 @@ public final class ShadowLinuxTest {
@Before
public void setUp() throws Exception {
shadowLinux = new ShadowLinux();
- file = File.createTempFile("ShadowLinuxTest", null);
+ file = tempFolder.newFile("ShadowLinuxTest");
path = file.getAbsolutePath();
try (FileOutputStream outputStream = new FileOutputStream(file)) {
- outputStream.write(1234);
+ outputStream.write("some UTF-8\u202Fcontent in a file".getBytes(UTF_8));
}
}
@@ -48,4 +56,51 @@ public final class ShadowLinuxTest {
StructStat stat = shadowLinux.stat(path);
assertThat(stat.st_mtime).isEqualTo(Duration.ofMillis(file.lastModified()).getSeconds());
}
+
+ @Test
+ public void pread_validateExtractsContentWithOffset() throws Exception {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ FileDescriptor fd = fis.getFD();
+ assertThat(fd.valid()).isTrue();
+
+ final int bytesCount = "content".length();
+ final int bytesOffset = 5;
+ final byte[] buffer = new byte[bytesCount + 2 * bytesOffset];
+ Arrays.fill(buffer, (byte) '-');
+
+ final int offsetInFile = "some UTF-8\u202F".getBytes(UTF_8).length;
+
+ assertThat(shadowLinux.pread(fd, buffer, bytesOffset, bytesCount, offsetInFile))
+ .isEqualTo(bytesCount);
+ assertThat(new String(buffer, UTF_8)).isEqualTo("-----content-----");
+ }
+ }
+
+ @Test
+ public void pread_handleFNF() throws Exception {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ FileDescriptor fd = fis.getFD();
+ assertThat(fd.valid()).isTrue();
+
+ // Delete the file under test.
+ fis.close();
+ assertThat(file.delete()).isTrue();
+
+ final byte[] buffer = new byte[10];
+ Arrays.fill(buffer, (byte) '-');
+ assertThat(shadowLinux.pread(fd, buffer, 0, 5, 0)).isEqualTo(-1);
+ }
+ }
+
+ @Test
+ public void pread_readPastEnd() throws Exception {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ FileDescriptor fd = fis.getFD();
+ assertThat(fd.valid()).isTrue();
+
+ final byte[] buffer = new byte[10];
+ Arrays.fill(buffer, (byte) '-');
+ assertThat(shadowLinux.pread(fd, buffer, 0, 5, 500)).isEqualTo(-1);
+ }
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLocationManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLocationManagerTest.java
index 5f46bbae6..5add1c81c 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLocationManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLocationManagerTest.java
@@ -28,6 +28,8 @@ import android.content.IntentFilter;
import android.location.Criteria;
import android.location.GnssAntennaInfo;
import android.location.GnssAntennaInfo.PhaseCenterOffset;
+import android.location.GnssMeasurementRequest;
+import android.location.GnssMeasurementsEvent;
import android.location.GnssStatus;
import android.location.GpsStatus;
import android.location.Location;
@@ -1161,6 +1163,65 @@ public class ShadowLocationManagerTest {
.inOrder();
}
+ @Config(minSdk = VERSION_CODES.S)
+ @Test
+ public void testRequestFlush_listener() {
+ TestLocationListener listener = new TestLocationListener();
+
+ try {
+ locationManager.requestFlush(GPS_PROVIDER, listener, 0);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(listener.flushes).isEmpty();
+ }
+
+ locationManager.requestLocationUpdates(GPS_PROVIDER, 0, 0, listener);
+ locationManager.requestFlush(GPS_PROVIDER, listener, 1);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(listener.flushes).containsExactly(1);
+ }
+
+ @Config(minSdk = VERSION_CODES.S)
+ @Test
+ public void testRequestFlush_pendingIntent() {
+ TestLocationReceiver listener = new TestLocationReceiver(context);
+
+ try {
+ locationManager.requestFlush(GPS_PROVIDER, listener.pendingIntent, 0);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(listener.flushes).isEmpty();
+ }
+
+ locationManager.requestLocationUpdates(GPS_PROVIDER, 0, 0, listener.pendingIntent);
+ locationManager.requestFlush(GPS_PROVIDER, listener.pendingIntent, 1);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(listener.flushes).containsExactly(1);
+ }
+
+ @Config(minSdk = VERSION_CODES.S)
+ @Test
+ public void testRequestFlush_pendingIntent_canceled() {
+ TestLocationReceiver listener = new TestLocationReceiver(context);
+
+ try {
+ locationManager.requestFlush(GPS_PROVIDER, listener.pendingIntent, 0);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(listener.flushes).isEmpty();
+ }
+
+ locationManager.requestLocationUpdates(GPS_PROVIDER, 0, 0, listener.pendingIntent);
+ listener.pendingIntent.cancel();
+ locationManager.requestFlush(GPS_PROVIDER, listener.pendingIntent, 1);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(shadowLocationManager.getLocationRequests(GPS_PROVIDER)).isEmpty();
+ assertThat(listener.flushes).isEmpty();
+ }
+
@Test
public void testSimulateLocation_FastestInterval() {
Location loc1 = createLocation(MY_PROVIDER);
@@ -1206,6 +1267,26 @@ public class ShadowLocationManagerTest {
}
@Test
+ public void testSimulateLocation_Batch() {
+ Location loc1 = createLocation(MY_PROVIDER);
+ Location loc2 = createLocation(MY_PROVIDER);
+
+ TestLocationListener myListener = new TestLocationListener();
+
+ locationManager.requestLocationUpdates(MY_PROVIDER, 0, 0, myListener);
+ shadowLocationManager.simulateLocation(MY_PROVIDER, loc1, loc2);
+ locationManager.removeUpdates(myListener);
+
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(myListener.locations)
+ .comparingElementsUsing(equality())
+ .containsExactly(loc1, loc2)
+ .inOrder();
+ assertThat(locationManager.getLastKnownLocation(MY_PROVIDER)).isEqualTo(loc2);
+ }
+
+ @Test
public void testLocationUpdates_NullListener() {
try {
locationManager.requestSingleUpdate(GPS_PROVIDER, null, null);
@@ -1330,6 +1411,14 @@ public class ShadowLocationManagerTest {
}
@Test
+ @Config(minSdk = VERSION_CODES.O)
+ public void testGetGnssBatchSize() {
+ assertThat(locationManager.getGnssBatchSize()).isEqualTo(0);
+ shadowLocationManager.setGnssBatchSize(5);
+ assertThat(locationManager.getGnssBatchSize()).isEqualTo(5);
+ }
+
+ @Test
@Config(minSdk = VERSION_CODES.P)
public void testGetGnssHardwareModelName() {
assertThat(locationManager.getGnssHardwareModelName()).isNull();
@@ -1442,6 +1531,38 @@ public class ShadowLocationManagerTest {
}
@Test
+ @Config(minSdk = VERSION_CODES.TIRAMISU)
+ public void testGnssMeasurementsCallback() {
+ GnssMeasurementsEvent.Callback listener1 = mock(GnssMeasurementsEvent.Callback.class);
+ GnssMeasurementsEvent.Callback listener2 = mock(GnssMeasurementsEvent.Callback.class);
+ InOrder inOrder1 = Mockito.inOrder(listener1);
+ InOrder inOrder2 = Mockito.inOrder(listener2);
+
+ GnssMeasurementsEvent events1 = new GnssMeasurementsEvent.Builder().build();
+ GnssMeasurementsEvent events2 = new GnssMeasurementsEvent.Builder().build();
+
+ locationManager.registerGnssMeasurementsCallback(Runnable::run, listener1);
+ locationManager.registerGnssMeasurementsCallback(
+ new GnssMeasurementRequest.Builder().build(), Runnable::run, listener2);
+
+ shadowLocationManager.simulateGnssMeasurementsEvent(events1);
+ inOrder1.verify(listener1).onGnssMeasurementsReceived(events1);
+ inOrder2.verify(listener2).onGnssMeasurementsReceived(events1);
+
+ locationManager.unregisterGnssMeasurementsCallback(listener2);
+
+ shadowLocationManager.simulateGnssMeasurementsEvent(events2);
+ inOrder1.verify(listener1).onGnssMeasurementsReceived(events2);
+ inOrder2.verify(listener2, never()).onGnssMeasurementsReceived(events2);
+
+ locationManager.unregisterGnssMeasurementsCallback(listener1);
+
+ shadowLocationManager.simulateGnssMeasurementsEvent(events1);
+ inOrder1.verify(listener1, never()).onGnssMeasurementsReceived(events1);
+ inOrder2.verify(listener2, never()).onGnssMeasurementsReceived(events1);
+ }
+
+ @Test
@Config(minSdk = VERSION_CODES.R)
public void testGnssAntennaInfoListener() {
GnssAntennaInfo.Listener listener1 = mock(GnssAntennaInfo.Listener.class);
@@ -1606,6 +1727,7 @@ public class ShadowLocationManagerTest {
private final PendingIntent pendingIntent;
private final ArrayList<Boolean> providerEnableds = new ArrayList<>();
private final ArrayList<Location> locations = new ArrayList<>();
+ private final ArrayList<Integer> flushes = new ArrayList<>();
private TestLocationReceiver(Context context) {
Intent intent = new Intent(Integer.toString(random.nextInt()));
@@ -1621,6 +1743,9 @@ public class ShadowLocationManagerTest {
if (intent.hasExtra(LocationManager.KEY_PROVIDER_ENABLED)) {
providerEnableds.add(intent.getBooleanExtra(LocationManager.KEY_PROVIDER_ENABLED, false));
}
+ if (intent.hasExtra(LocationManager.KEY_FLUSH_COMPLETE)) {
+ flushes.add(intent.getIntExtra(LocationManager.KEY_FLUSH_COMPLETE, -1));
+ }
}
}
@@ -1636,6 +1761,7 @@ public class ShadowLocationManagerTest {
private static class TestLocationListener implements LocationListener {
final ArrayList<Boolean> providerEnableds = new ArrayList<>();
final ArrayList<Location> locations = new ArrayList<>();
+ final ArrayList<Integer> flushes = new ArrayList<>();
@Override
public void onLocationChanged(Location location) {
@@ -1655,6 +1781,11 @@ public class ShadowLocationManagerTest {
public void onProviderDisabled(String s) {
providerEnableds.add(false);
}
+
+ @Override
+ public void onFlushComplete(int requestCode) {
+ flushes.add(requestCode);
+ }
}
private static class TestLocationListenerSelfRemoval extends TestLocationListener {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLooperResetterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLooperResetterTest.java
new file mode 100644
index 000000000..92e1643c5
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLooperResetterTest.java
@@ -0,0 +1,191 @@
+package org.robolectric.shadows;
+
+import static android.os.Looper.getMainLooper;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
+
+/** A specialized test for verifying that looper state is cleared properly between tests. */
+@RunWith(JUnit4.class)
+public class ShadowLooperResetterTest {
+ private final RunNotifier runNotifier = new RunNotifier();
+
+ @Before
+ public void setup() {
+ runNotifier.addListener(
+ new RunListener() {
+ @Override
+ public void testFailure(Failure failure) throws Exception {
+ throw new AssertionError("Unexpected test failure: " + failure, failure.getException());
+ }
+ });
+ }
+
+ /**
+ * Basic test class that interacts with Looper in two different tests, to ensure Looper remains
+ * functional after reset.
+ */
+ public static class BasicLooperTest {
+
+ private void doPostToLooperTest() {
+ checkNotNull(getMainLooper());
+
+ AtomicBoolean didRun = new AtomicBoolean(false);
+ new Handler(getMainLooper()).post(() -> didRun.set(true));
+
+ assertThat(didRun.get()).isFalse();
+ shadowOf(getMainLooper()).idle();
+ assertThat(didRun.get()).isTrue();
+ }
+
+ @Test
+ public void postToLooperTest() {
+ doPostToLooperTest();
+ }
+
+ @Test
+ public void anotherPostToLooperTest() {
+ doPostToLooperTest();
+ }
+ }
+
+ @Test
+ public void basicPostAndRun() throws InitializationError {
+ Runner runner = new RobolectricTestRunner(BasicLooperTest.class);
+
+ // run and assert no failures
+ runner.run(runNotifier);
+ }
+
+ /** Test that leaves an unexecuted runnable on Looper and verifies it is removed between tests. */
+ public static class PendingLooperTest {
+
+ private void doPostToLooperTest() {
+ checkState(shadowOf(getMainLooper()).isIdle());
+
+ AtomicBoolean didRun = new AtomicBoolean(false);
+ new Handler(getMainLooper()).post(() -> didRun.set(true));
+
+ assertThat(didRun.get()).isFalse();
+ assertThat(shadowOf(getMainLooper()).isIdle()).isFalse();
+ }
+
+ @Test
+ public void postToLooperTest() {
+ doPostToLooperTest();
+ }
+
+ @Test
+ public void anotherPostToLooperTest() {
+ doPostToLooperTest();
+ }
+ }
+
+ @Test
+ public void pendingTasksClearer() throws InitializationError {
+ Runner runner = new RobolectricTestRunner(PendingLooperTest.class);
+
+ // run and assert no failures
+ runner.run(runNotifier);
+ }
+
+ /** Test that uses delayed tasks */
+ public static class DelayedTaskTest {
+
+ private void doDelayedPostToLooperTest() {
+ checkState(shadowOf(getMainLooper()).isIdle());
+
+ AtomicBoolean didRun = new AtomicBoolean(false);
+ new Handler(getMainLooper()).postDelayed(() -> didRun.set(true), 100);
+ shadowOf(getMainLooper()).idle();
+ assertThat(didRun.get()).isFalse();
+ shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
+ assertThat(didRun.get()).isTrue();
+ }
+
+ @Test
+ public void postToLooperTest() {
+ doDelayedPostToLooperTest();
+ }
+
+ @Test
+ public void anotherPostToLooperTest() {
+ doDelayedPostToLooperTest();
+ }
+ }
+
+ @Test
+ public void delayedTask() throws InitializationError {
+ Runner runner = new RobolectricTestRunner(DelayedTaskTest.class);
+
+ // run and assert no failures
+ runner.run(runNotifier);
+ }
+
+ /** Test that uses delayed tasks on a running looper */
+ public static class DelayedTaskRunningLooperTest {
+
+ // use a static thread so both tests share the same looper
+ static HandlerThread handlerThread;
+
+ @Before
+ public void init() {
+ if (handlerThread == null) {
+ handlerThread = new HandlerThread("DelayedTaskRunningLooperTest");
+ handlerThread.start();
+ }
+ }
+
+ @AfterClass
+ public static void shutDown() throws InterruptedException {
+ handlerThread.quit();
+ handlerThread.join();
+ }
+
+ private void doDelayedPostToLooperTest() {
+ checkNotNull(handlerThread.getLooper());
+
+ AtomicBoolean didRun = new AtomicBoolean(false);
+ new Handler(handlerThread.getLooper()).postDelayed(() -> didRun.set(true), 100);
+ shadowOf(handlerThread.getLooper()).idle();
+ assertThat(didRun.get()).isFalse();
+ shadowOf(handlerThread.getLooper()).idleFor(Duration.ofMillis(100));
+ assertThat(didRun.get()).isTrue();
+ }
+
+ @Test
+ public void postToLooperTest() {
+ doDelayedPostToLooperTest();
+ }
+
+ @Test
+ public void anotherPostToLooperTest() {
+ doDelayedPostToLooperTest();
+ }
+ }
+
+ @Test
+ public void delayedTaskRunningLooper() throws InitializationError {
+ Runner runner = new RobolectricTestRunner(DelayedTaskRunningLooperTest.class);
+
+ // run and assert no failures
+ runner.run(runNotifier);
+ }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
index 5d43069b5..d913c9927 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowNfcAdapterTest.java
@@ -1,10 +1,12 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.app.Activity;
import android.app.Application;
@@ -24,6 +26,7 @@ import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import org.robolectric.util.reflector.ForType;
@RunWith(AndroidJUnit4.class)
public class ShadowNfcAdapterTest {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
index e3242fadd..12957335b 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPackageManagerTest.java
@@ -963,7 +963,7 @@ public class ShadowPackageManagerTest {
}
@Test
- public void getApplicationInfo_ThisApplication() throws Exception {
+ public void getApplicationInfo_thisApplication() throws Exception {
ApplicationInfo info = packageManager.getApplicationInfo(context.getPackageName(), 0);
assertThat(info).isNotNull();
assertThat(info.packageName).isEqualTo(context.getPackageName());
@@ -971,6 +971,16 @@ public class ShadowPackageManagerTest {
}
@Test
+ @Config(minSdk = TIRAMISU)
+ public void getApplicationInfo_thisApplication_withApplicationInfoFlags() throws Exception {
+ ApplicationInfo info =
+ packageManager.getApplicationInfo(context.getPackageName(), ApplicationInfoFlags.of(0));
+ assertThat(info).isNotNull();
+ assertThat(info.packageName).isEqualTo(context.getPackageName());
+ assertThat(info.processName).isEqualTo(info.packageName);
+ }
+
+ @Test
public void getApplicationInfo_uninstalledApplication_includeUninstalled() throws Exception {
shadowOf(packageManager).deletePackage(context.getPackageName());
@@ -981,6 +991,20 @@ public class ShadowPackageManagerTest {
}
@Test
+ @Config(minSdk = TIRAMISU)
+ public void
+ getApplicationInfo_uninstalledApplication_includeUninstalled_withApplicationInfoFlags()
+ throws Exception {
+ shadowOf(packageManager).deletePackage(context.getPackageName());
+
+ ApplicationInfo info =
+ packageManager.getApplicationInfo(
+ context.getPackageName(), ApplicationInfoFlags.of(MATCH_UNINSTALLED_PACKAGES));
+ assertThat(info).isNotNull();
+ assertThat(info.packageName).isEqualTo(context.getPackageName());
+ }
+
+ @Test
public void getApplicationInfo_uninstalledApplication_dontIncludeUninstalled() {
shadowOf(packageManager).deletePackage(context.getPackageName());
@@ -992,6 +1016,20 @@ public class ShadowPackageManagerTest {
}
}
+ @Test
+ @Config(minSdk = TIRAMISU)
+ public void
+ getApplicationInfo_uninstalledApplication_dontIncludeUninstalled_withApplicationInfoFlags() {
+ shadowOf(packageManager).deletePackage(context.getPackageName());
+
+ try {
+ packageManager.getApplicationInfo(context.getPackageName(), ApplicationInfoFlags.of(0));
+ fail("PackageManager.NameNotFoundException not thrown");
+ } catch (PackageManager.NameNotFoundException e) {
+ // expected
+ }
+ }
+
@Test(expected = PackageManager.NameNotFoundException.class)
public void getApplicationInfo_whenUnknown_shouldThrowNameNotFoundException() throws Exception {
try {
@@ -1003,8 +1041,29 @@ public class ShadowPackageManagerTest {
}
}
+ @Test(expected = PackageManager.NameNotFoundException.class)
+ @Config(minSdk = TIRAMISU)
+ public void
+ getApplicationInfo_whenUnknown_shouldThrowNameNotFoundException_withApplicationInfoFlags()
+ throws Exception {
+ try {
+ packageManager.getApplicationInfo("unknown_package", ApplicationInfoFlags.of(0));
+ fail("should have thrown NameNotFoundException");
+ } catch (PackageManager.NameNotFoundException e) {
+ assertThat(e.getMessage()).contains("unknown_package");
+ throw e;
+ }
+ }
+
+ @Test
+ public void getApplicationInfo_nullPackage_shouldThrowNameNotFoundException() {
+ assertThrows(
+ PackageManager.NameNotFoundException.class,
+ () -> packageManager.getApplicationInfo(null, 0));
+ }
+
@Test
- public void getApplicationInfo_OtherApplication() throws Exception {
+ public void getApplicationInfo_otherApplication() throws Exception {
PackageInfo packageInfo = new PackageInfo();
packageInfo.packageName = TEST_PACKAGE_NAME;
packageInfo.applicationInfo = new ApplicationInfo();
@@ -1019,6 +1078,23 @@ public class ShadowPackageManagerTest {
}
@Test
+ @Config(minSdk = TIRAMISU)
+ public void getApplicationInfo_otherApplication_withApplicationInfoFlags() throws Exception {
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = TEST_PACKAGE_NAME;
+ packageInfo.applicationInfo = new ApplicationInfo();
+ packageInfo.applicationInfo.packageName = TEST_PACKAGE_NAME;
+ packageInfo.applicationInfo.name = TEST_PACKAGE_LABEL;
+ shadowOf(packageManager).installPackage(packageInfo);
+
+ ApplicationInfo info =
+ packageManager.getApplicationInfo(TEST_PACKAGE_NAME, ApplicationInfoFlags.of(0));
+ assertThat(info).isNotNull();
+ assertThat(info.packageName).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(packageManager.getApplicationLabel(info).toString()).isEqualTo(TEST_PACKAGE_LABEL);
+ }
+
+ @Test
public void getApplicationInfo_readsValuesFromSetPackageArchiveInfo() {
PackageInfo packageInfo = new PackageInfo();
packageInfo.packageName = "some.package.name";
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java
index 062e73ab0..c0ff1622f 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSpeechRecognizerTest.java
@@ -121,6 +121,18 @@ public class ShadowSpeechRecognizerTest {
assertNoErrorLogs();
}
+ /** Verify that isDestroyed is corrected set by destroy and unset by startListening */
+ @Test
+ public void startListeningThenDestroyAndStartListening() {
+ startListening();
+ assertThat(shadowOf(speechRecognizer).isDestroyed()).isFalse();
+ speechRecognizer.destroy();
+ shadowOf(getMainLooper()).idle();
+ assertThat(shadowOf(speechRecognizer).isDestroyed()).isTrue();
+ startListening();
+ assertThat(shadowOf(speechRecognizer).isDestroyed()).isFalse();
+ }
+
/** Verify the startlistening flow works when using custom component name. */
@Test
public void startListeningWithCustomComponent() {
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
index cb6359f8e..8571627a4 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
@@ -45,6 +45,8 @@ import static org.robolectric.RuntimeEnvironment.getApplication;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowTelephonyManager.createTelephonyDisplayInfo;
+import android.Manifest.permission;
+import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -101,6 +103,8 @@ public class ShadowTelephonyManagerTest {
public void setUp() throws Exception {
telephonyManager = (TelephonyManager) getApplication().getSystemService(TELEPHONY_SERVICE);
shadowTelephonyManager = Shadow.extract(telephonyManager);
+ shadowOf((Application) ApplicationProvider.getApplicationContext())
+ .grantPermissions(permission.READ_PRIVILEGED_PHONE_STATE);
}
@Test
@@ -1040,4 +1044,28 @@ public class ShadowTelephonyManagerTest {
assertThrows(IllegalStateException.class, () -> telephonyManager.isEmergencyNumber("911"));
}
+
+ @Test
+ @Config(minSdk = O)
+ public void
+ getEmergencyCallbackMode_noReadPrivilegedPhoneStatePermission_throwsSecurityException() {
+ shadowOf((Application) ApplicationProvider.getApplicationContext())
+ .denyPermissions(permission.READ_PRIVILEGED_PHONE_STATE);
+
+ assertThrows(SecurityException.class, () -> telephonyManager.getEmergencyCallbackMode());
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void getEmergencyCallback_wasSetToTrue_returnsTrue() {
+ shadowTelephonyManager.setEmergencyCallbackMode(true);
+
+ assertThat(telephonyManager.getEmergencyCallbackMode()).isTrue();
+ }
+
+ @Test
+ @Config(minSdk = O)
+ public void getEmergencyCallback_notSet_returnsFalse() {
+ assertThat(telephonyManager.getEmergencyCallbackMode()).isFalse();
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowViewGroupTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowViewGroupTest.java
index 488b55e08..ad74bb4fc 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowViewGroupTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowViewGroupTest.java
@@ -14,8 +14,8 @@ import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
+import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -24,6 +24,7 @@ import android.view.animation.Animation.AnimationListener;
import android.view.animation.LayoutAnimationController;
import android.widget.FrameLayout;
import android.widget.TextView;
+import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.ByteArrayOutputStream;
@@ -34,6 +35,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.R;
import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
public class ShadowViewGroupTest {
@@ -182,32 +184,71 @@ public class ShadowViewGroupTest {
}
@Test
+ @Config(minSdk = 17) // TODO: mysteriously fails on github CI on API 16
public void hasFocus_shouldReturnTrueIfAnyChildHasFocus() {
- makeFocusable(root, child1, child2, child3, child3a, child3b);
- assertFalse(root.hasFocus());
+ ContainerActivity containerActivity = Robolectric.setupActivity(ContainerActivity.class);
+ makeFocusable(
+ containerActivity.root,
+ containerActivity.child1,
+ containerActivity.child2,
+ containerActivity.child3,
+ containerActivity.child3a,
+ containerActivity.child3b);
- child1.requestFocus();
- assertTrue(root.hasFocus());
+ containerActivity.child1.requestFocus();
+ assertTrue(containerActivity.root.hasFocus());
- child1.clearFocus();
- assertFalse(child1.hasFocus());
- assertTrue(root.hasFocus());
+ containerActivity.child1.clearFocus();
+ assertFalse(containerActivity.child1.hasFocus());
+ assertTrue(containerActivity.root.hasFocus());
- child3b.requestFocus();
- assertTrue(root.hasFocus());
+ containerActivity.child3b.requestFocus();
+ assertTrue(containerActivity.root.hasFocus());
- child3b.clearFocus();
- assertFalse(child3b.hasFocus());
- assertFalse(child3.hasFocus());
- assertTrue(root.hasFocus());
+ containerActivity.child3b.clearFocus();
+ assertFalse(containerActivity.child3b.hasFocus());
+ assertFalse(containerActivity.child3.hasFocus());
+ assertTrue(containerActivity.root.hasFocus());
- child2.requestFocus();
- assertFalse(child3.hasFocus());
- assertTrue(child2.hasFocus());
- assertTrue(root.hasFocus());
+ containerActivity.child2.requestFocus();
+ assertFalse(containerActivity.child3.hasFocus());
+ assertTrue(containerActivity.child2.hasFocus());
+ assertTrue(containerActivity.root.hasFocus());
- root.requestFocus();
- assertTrue(root.hasFocus());
+ containerActivity.root.requestFocus();
+ assertTrue(containerActivity.root.hasFocus());
+ }
+
+ private static class ContainerActivity extends Activity {
+
+ ViewGroup root;
+ View child1;
+ View child2;
+ ViewGroup child3;
+ View child3a;
+ View child3b;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ root = new FrameLayout(this);
+
+ child1 = new View(this);
+ child2 = new View(this);
+ child3 = new FrameLayout(this);
+ child3a = new View(this);
+ child3b = new View(this);
+
+ root.addView(child1);
+ root.addView(child2);
+ root.addView(child3);
+
+ child3.addView(child3a);
+ child3.addView(child3b);
+
+ setContentView(root);
+ }
}
@Test
@@ -455,7 +496,7 @@ public class ShadowViewGroupTest {
DrawRecordView view = new DrawRecordView(context);
ViewGroup viewGroup = new FrameLayout(context);
viewGroup.addView(view);
- Bitmap bitmap = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
+ Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
viewGroup.draw(canvas);
assertThat(view.wasDrawn).isTrue();
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
index 299a85333..7a1bee691 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
@@ -14,11 +14,15 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
+import android.app.admin.DeviceAdminService;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.DhcpInfo;
import android.net.NetworkInfo;
import android.net.wifi.ScanResult;
+import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
@@ -226,11 +230,11 @@ public class ShadowWifiManagerTest {
assertThat(wifiManager.updateNetwork(wifiConfiguration)).isEqualTo(networkId);
// If we don't have permission to update, updateNetwork will return -1.
- shadowOf(wifiManager).setUpdateNetworkPermission(networkId, /* hasPermission = */ false);
+ shadowOf(wifiManager).setUpdateNetworkPermission(networkId, /* hasPermission= */ false);
assertThat(wifiManager.updateNetwork(wifiConfiguration)).isEqualTo(-1);
// Ensure updates can occur if permission is restored.
- shadowOf(wifiManager).setUpdateNetworkPermission(networkId, /* hasPermission = */ true);
+ shadowOf(wifiManager).setUpdateNetworkPermission(networkId, /* hasPermission= */ true);
assertThat(wifiManager.updateNetwork(wifiConfiguration)).isEqualTo(networkId);
}
@@ -764,4 +768,29 @@ public class ShadowWifiManagerTest {
assertThat(shadowOf(wifiManager).getWifiApConfiguration().SSID).isEqualTo("foo");
}
+
+ @Test
+ @Config(minSdk = R)
+ public void shouldRecordTheLastSoftApConfiguration() {
+ SoftApConfiguration softApConfig =
+ new SoftApConfiguration.Builder()
+ .setSsid("foo")
+ .setPassphrase(null, SoftApConfiguration.SECURITY_TYPE_OPEN)
+ .build();
+
+ boolean status = wifiManager.setSoftApConfiguration(softApConfig);
+ assertThat(status).isTrue();
+
+ assertThat(shadowOf(wifiManager).getSoftApConfiguration().getSsid()).isEqualTo("foo");
+ }
+
+ private void setDeviceOwner() {
+ shadowOf(
+ (DevicePolicyManager)
+ ApplicationProvider.getApplicationContext()
+ .getSystemService(Context.DEVICE_POLICY_SERVICE))
+ .setDeviceOwner(
+ new ComponentName(
+ ApplicationProvider.getApplicationContext(), DeviceAdminService.class));
+ }
}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/SharedLibraryInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/SharedLibraryInfoBuilderTest.java
new file mode 100644
index 000000000..2d5f0d6a5
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/SharedLibraryInfoBuilderTest.java
@@ -0,0 +1,49 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.pm.SharedLibraryInfo;
+import android.os.Build.VERSION_CODES;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link SharedLibraryInfoBuilder}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = VERSION_CODES.O)
+public final class SharedLibraryInfoBuilderTest {
+
+ @Test
+ @Config(maxSdk = VERSION_CODES.P)
+ public void build_beforeVersionQ() {
+
+ SharedLibraryInfo sharedLibraryInfo =
+ SharedLibraryInfoBuilder.newBuilder()
+ .setName("trichromelibrary")
+ .setVersion(0)
+ .setType(SharedLibraryInfo.TYPE_STATIC)
+ .build();
+
+ assertThat(sharedLibraryInfo.getType()).isEqualTo(SharedLibraryInfo.TYPE_STATIC);
+ assertThat(sharedLibraryInfo.getName()).isEqualTo("trichromelibrary");
+ assertThat(sharedLibraryInfo.getVersion()).isEqualTo(0);
+ }
+
+ @Test
+ @Config(minSdk = VERSION_CODES.Q)
+ public void build_versionQ() {
+
+ SharedLibraryInfo sharedLibraryInfo =
+ SharedLibraryInfoBuilder.newBuilder()
+ .setName("com.google.android.trichromelibrary_535912833")
+ .setVersion(535912833)
+ .setType(SharedLibraryInfo.TYPE_STATIC)
+ .build();
+
+ assertThat(sharedLibraryInfo.getType()).isEqualTo(SharedLibraryInfo.TYPE_STATIC);
+ assertThat(sharedLibraryInfo.getName())
+ .isEqualTo("com.google.android.trichromelibrary_535912833");
+ assertThat(sharedLibraryInfo.getVersion()).isEqualTo(535912833);
+ }
+}
diff --git a/robolectric/src/test/resources/res/menu/action_menu.xml b/robolectric/src/test/resources/res/menu/action_menu.xml
index b071173f9..47ae17e6d 100644
--- a/robolectric/src/test/resources/res/menu/action_menu.xml
+++ b/robolectric/src/test/resources/res/menu/action_menu.xml
@@ -1,5 +1,7 @@
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="AppCompatResource">
<item android:id="@+id/action_search"
- android:actionViewClass="android.widget.SearchView"
- android:showAsAction="always"/>
+ android:actionViewClass="android.widget.SearchView"
+ android:showAsAction="always"/>
</menu>
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 00e200941..fac00226d 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
@@ -225,6 +225,10 @@ public class ClassInstrumentor {
&& ((LdcInsnNode) insns[0]).cst instanceof ConstantDynamic) {
ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) insns[0]).cst;
return cst.getName().equals("$jacocoData");
+ } else if (insns.length > 1
+ && insns[0] instanceof LabelNode
+ && insns[1] instanceof MethodInsnNode) {
+ return "$jacocoInit".equals(((MethodInsnNode) insns[1]).name);
}
}
return false;
diff --git a/settings.gradle b/settings.gradle
index 9a187d806..20f8fae14 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -22,6 +22,7 @@ include ":nativeruntime"
include ":integration_tests:agp"
include ":integration_tests:agp:testsupport"
include ":integration_tests:dependency-on-stubs"
+include ":integration_tests:kotlin"
include ":integration_tests:libphonenumber"
include ":integration_tests:memoryleaks"
include ":integration_tests:mockito"
@@ -33,7 +34,10 @@ include ':integration_tests:androidx_test'
include ':integration_tests:ctesque'
include ':integration_tests:security-providers'
include ":integration_tests:mockk"
+include ":integration_tests:jacoco-offline"
include ':integration_tests:compat-target28'
include ":integration_tests:multidex"
+include ":integration_tests:play_services"
include ":integration_tests:sparsearray"
+include ":integration_tests:nativegraphics"
include ':testapp'
diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle
index cd95bb106..a273d5a6f 100644
--- a/shadows/framework/build.gradle
+++ b/shadows/framework/build.gradle
@@ -53,10 +53,10 @@ dependencies {
compileOnly "com.google.code.findbugs:jsr305:3.0.2"
api "com.almworks.sqlite4java:sqlite4java:$sqlite4javaVersion"
compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
- api "com.ibm.icu:icu4j:70.1"
+ api "com.ibm.icu:icu4j:72.1"
api "androidx.annotation:annotation:1.1.0"
- api "com.google.auto.value:auto-value-annotations:1.10"
- annotationProcessor "com.google.auto.value:auto-value:1.9"
+ api "com.google.auto.value:auto-value-annotations:1.10.1"
+ annotationProcessor "com.google.auto.value:auto-value:1.10.1"
sqlite4java "com.almworks.sqlite4java:libsqlite4java-osx:$sqlite4javaVersion"
sqlite4java "com.almworks.sqlite4java:libsqlite4java-linux-amd64:$sqlite4javaVersion"
diff --git a/shadows/framework/src/main/java/android/webkit/RoboCookieManager.java b/shadows/framework/src/main/java/android/webkit/RoboCookieManager.java
index 2fc58d072..b9626e882 100644
--- a/shadows/framework/src/main/java/android/webkit/RoboCookieManager.java
+++ b/shadows/framework/src/main/java/android/webkit/RoboCookieManager.java
@@ -10,6 +10,7 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+import java.util.Objects;
import javax.annotation.Nullable;
/**
@@ -29,6 +30,20 @@ public class RoboCookieManager extends CookieManager {
public void setCookie(String url, String value) {
Cookie cookie = parseCookie(url, value);
if (cookie != null) {
+ Cookie existingCookie = null;
+ for (Cookie c : store) {
+ if (c == null) {
+ continue;
+ }
+ if (Objects.equals(c.getName(), cookie.getName())
+ && Objects.equals(c.mHostname, cookie.mHostname)) {
+ existingCookie = c;
+ break;
+ }
+ }
+ if (existingCookie != null) {
+ store.remove(existingCookie);
+ }
store.add(cookie);
}
}
@@ -208,7 +223,7 @@ public class RoboCookieManager extends CookieManager {
String field = fields[i].trim();
if (field.startsWith(EXPIRATION_FIELD_NAME)) {
expiration = getExpiration(field);
- } else if (field.toUpperCase().equals(SECURE_ATTR_NAME)) {
+ } else if (field.equalsIgnoreCase(SECURE_ATTR_NAME)) {
isSecure = true;
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java
index 803930ce3..74d5b3f81 100644
--- a/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java
+++ b/shadows/framework/src/main/java/org/robolectric/android/controller/ActivityController.java
@@ -5,6 +5,7 @@ import static android.os.Build.VERSION_CODES.N_MR1;
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.TIRAMISU;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.robolectric.shadow.api.Shadow.extract;
import static org.robolectric.util.reflector.Reflector.reflector;
@@ -147,8 +148,10 @@ public class ActivityController<T extends Activity>
() -> {
if (RuntimeEnvironment.getApiLevel() <= O_MR1) {
_component_.performRestart();
- } else {
+ } else if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
_component_.performRestart(true, "restart()");
+ } else {
+ _component_.performRestart(true);
}
currentState = LifecycleState.RESTARTED;
});
@@ -586,6 +589,23 @@ public class ActivityController<T extends Activity>
case STARTED:
resume();
// fall through
+ default:
+ // fall through
+ }
+
+ // Activity#mChangingConfigurations flag should be set prior to Activity recreation process
+ // starts. ActivityThread does set it on real device but here we simulate the Activity
+ // recreation process on behalf of ActivityThread so set the flag here. Note we don't need to
+ // reset the flag to false because this Activity instance is going to be destroyed and disposed.
+ // https://android.googlesource.com/platform/frameworks/base/+/55418eada51d4f5e6532ae9517af66c50
+ // ea495c4/core/java/android/app/ActivityThread.java#4806
+ _component_.setChangingConfigurations(true);
+
+ switch (originalState) {
+ case INITIAL:
+ case CREATED:
+ case RESTARTED:
+ case STARTED:
case RESUMED:
pause();
// fall through
@@ -598,14 +618,6 @@ public class ActivityController<T extends Activity>
throw new IllegalStateException("Cannot recreate activity since it's destroyed already");
}
- // Activity#mChangingConfigurations flag should be set prior to Activity recreation process
- // starts. ActivityThread does set it on real device but here we simulate the Activity
- // recreation process on behalf of ActivityThread so set the flag here. Note we don't need to
- // reset the flag to false because this Activity instance is going to be destroyed and disposed.
- // https://android.googlesource.com/platform/frameworks/base/+/55418eada51d4f5e6532ae9517af66c50
- // ea495c4/core/java/android/app/ActivityThread.java#4806
- _component_.setChangingConfigurations(true);
-
Bundle outState = new Bundle();
saveInstanceState(outState);
Object lastNonConfigurationInstances = _component_.retainNonConfigurationInstances();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java
new file mode 100644
index 000000000..54cccf067
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java
@@ -0,0 +1,68 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.media.AudioDeviceInfo;
+import android.os.Build.VERSION_CODES;
+import android.util.SparseIntArray;
+import androidx.annotation.RequiresApi;
+import java.util.Optional;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.Static;
+
+/** Builder for {@link AudioDeviceInfo}. */
+@RequiresApi(VERSION_CODES.M)
+public class AudioDeviceInfoBuilder {
+
+ private int type;
+
+ private AudioDeviceInfoBuilder() {}
+
+ public static AudioDeviceInfoBuilder newBuilder() {
+ return new AudioDeviceInfoBuilder();
+ }
+
+ /**
+ * Sets the device type.
+ *
+ * @param type The device type. The possible values are the constants defined as <a
+ * href=https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/AudioDeviceInfo.java?q=AudioDeviceType>
+ * {@code AudioDeviceInfo.AudioDeviceType}</a>
+ */
+ public AudioDeviceInfoBuilder setType(int type) {
+ this.type = type;
+ return this;
+ }
+
+ public AudioDeviceInfo build() {
+ Object port = Shadow.newInstanceOf("android.media.AudioDevicePort");
+ ReflectionHelpers.setField(port, "mType", externalToInternalType(type));
+
+ return ReflectionHelpers.callConstructor(
+ AudioDeviceInfo.class, ClassParameter.from(port.getClass(), port));
+ }
+
+ /** Accessor interface for {@link AudioDeviceInfo}'s internals. */
+ @ForType(AudioDeviceInfo.class)
+ interface AudioDeviceInfoReflector {
+
+ @Static
+ @Accessor("EXT_TO_INT_DEVICE_MAPPING")
+ SparseIntArray getExtToIntDeviceMapping();
+ }
+
+ private static int externalToInternalType(int externalType) {
+ return Optional.ofNullable(
+ reflector(AudioDeviceInfoReflector.class).getExtToIntDeviceMapping().get(externalType))
+ .orElseThrow(
+ () ->
+ new IllegalArgumentException(
+ "External type "
+ + externalType
+ + " does not have a mapping to an internal type defined."));
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
index 9d4f6ab11..883dd2cad 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
@@ -26,6 +26,7 @@ import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import javax.annotation.Nonnull;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
@@ -273,8 +274,8 @@ public class ShadowActivityThread {
@Resetter
public static void reset() {
- reflector(_ActivityThread_.class, RuntimeEnvironment.getActivityThread())
- .getActivities()
- .clear();
+ Object activityThread = RuntimeEnvironment.getActivityThread();
+ Objects.requireNonNull(activityThread, "ShadowActivityThread.reset: ActivityThread not set");
+ reflector(_ActivityThread_.class, activityThread).getActivities().clear();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java
index f420031c9..0d36bfc06 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAppOpsManager.java
@@ -7,8 +7,10 @@ import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static java.util.stream.Collectors.toSet;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
+import static org.robolectric.util.reflector.Reflector.reflector;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -59,6 +61,8 @@ import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
/** Shadow for {@link AppOpsManager}. */
@Implements(value = AppOpsManager.class, minSdk = KITKAT, looseSignatures = true)
@@ -146,14 +150,22 @@ public class ShadowAppOpsManager {
for (Key key : entry.getValue()) {
if (op == key.getOpCode()
&& (key.getPackageName() == null || key.getPackageName().equals(packageName))) {
- String[] sOpToString =
- ReflectionHelpers.getStaticField(AppOpsManager.class, "sOpToString");
- entry.getKey().onOpChanged(sOpToString[op], packageName);
+ entry.getKey().onOpChanged(getOpString(op), packageName);
}
}
}
}
+ protected String getOpString(int opCode) {
+ if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+ String[] sOpToString = ReflectionHelpers.getStaticField(AppOpsManager.class, "sOpToString");
+ return sOpToString[opCode];
+ } else {
+ Object[] sAppOpInfos = ReflectionHelpers.getStaticField(AppOpsManager.class, "sAppOpInfos");
+ return reflector(AppOpInfoReflector.class, sAppOpInfos[opCode]).getName();
+ }
+ }
+
/**
* Returns app op details for all packages for which one of {@link #setMode} methods was used to
* set the value of one of the given app ops (it does return those set to 'default' mode, while
@@ -633,4 +645,10 @@ public class ShadowAppOpsManager {
ReflectionHelpers.setStaticField(AppOpsManager.class, "sOnOpNotedCallback", null);
}
}
+
+ @ForType(className = "android.app.AppOpInfo")
+ interface AppOpInfoReflector {
+ @Accessor("name")
+ String getName();
+ }
}
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 ae8dfb894..4084825f5 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowApplicationPackageManager.java
@@ -68,6 +68,7 @@ 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.ComponentEnabledSetting;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.OnPermissionsChangedListener;
import android.content.pm.PackageManager.PackageInfoFlags;
@@ -97,6 +98,7 @@ import android.telecom.TelecomManager;
import android.util.Log;
import android.util.Pair;
import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
import java.io.File;
@@ -1005,7 +1007,7 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
@Implementation(minSdk = TIRAMISU)
protected List<ApplicationInfo> getInstalledApplications(Object flags) {
- return getInstalledApplications(((ApplicationInfoFlags) flags).getValue());
+ return getInstalledApplications(((PackageManager.ApplicationInfoFlags) flags).getValue());
}
private List<ApplicationInfo> getInstalledApplications(long flags) {
@@ -1393,6 +1395,14 @@ public class ShadowApplicationPackageManager extends ShadowPackageManager {
return packageInfo.applicationInfo;
}
+ @Implementation(minSdk = TIRAMISU)
+ protected ApplicationInfo getApplicationInfo(Object packageName, Object flagsObject)
+ throws NameNotFoundException {
+ Preconditions.checkArgument(flagsObject instanceof PackageManager.ApplicationInfoFlags);
+ PackageManager.ApplicationInfoFlags flags = (PackageManager.ApplicationInfoFlags) flagsObject;
+ return getApplicationInfo((String) packageName, (int) (flags).getValue());
+ }
+
private void applyFlagsToApplicationInfo(@Nullable ApplicationInfo appInfo, long flags)
throws NameNotFoundException {
if (appInfo == null) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager10.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager10.java
index d52b5144b..769d5dd46 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager10.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager10.java
@@ -4,6 +4,7 @@ import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static org.robolectric.res.android.ApkAssetsCookie.K_INVALID_COOKIE;
import static org.robolectric.res.android.ApkAssetsCookie.kInvalidCookie;
import static org.robolectric.res.android.Asset.SEEK_CUR;
@@ -584,7 +585,7 @@ public class ShadowArscAssetManager10 extends ShadowAssetManager.ArscBase {
// jint smallest_screen_width_dp, jint screen_width_dp,
// jint screen_height_dp, jint screen_layout, jint ui_mode,
// jint color_mode, jint major_version) {
- @Implementation(minSdk = P)
+ @Implementation(minSdk = P, maxSdk = TIRAMISU)
protected static void nativeSetConfiguration(
long ptr,
int mcc,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager9.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager9.java
index 1db172c81..e79fa2a9f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager9.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager9.java
@@ -3,6 +3,7 @@ package org.robolectric.shadows;
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 static org.robolectric.res.android.ApkAssetsCookie.K_INVALID_COOKIE;
import static org.robolectric.res.android.ApkAssetsCookie.kInvalidCookie;
import static org.robolectric.res.android.Asset.SEEK_CUR;
@@ -578,7 +579,7 @@ public class ShadowArscAssetManager9 extends ShadowAssetManager.ArscBase {
// jint smallest_screen_width_dp, jint screen_width_dp,
// jint screen_height_dp, jint screen_layout, jint ui_mode,
// jint color_mode, jint major_version) {
- @Implementation(minSdk = P)
+ @Implementation(minSdk = P, maxSdk = TIRAMISU)
protected static void nativeSetConfiguration(
long ptr,
int mcc,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java
index d2215ca9b..0d13c918d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowClipboardManager.java
@@ -2,6 +2,7 @@ package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.N;
+import static android.os.Build.VERSION_CODES.P;
import static org.robolectric.RuntimeEnvironment.getApiLevel;
import static org.robolectric.util.reflector.Reflector.reflector;
@@ -44,6 +45,11 @@ public class ShadowClipboardManager {
}
}
+ @Implementation(minSdk = P)
+ protected void clearPrimaryClip() {
+ setPrimaryClip(null);
+ }
+
@Implementation
protected ClipData getPrimaryClip() {
return clip;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java
index a994fa24f..d60c68aae 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowConnectivityManager.java
@@ -8,6 +8,7 @@ import static android.os.Build.VERSION_CODES.O;
import static org.robolectric.RuntimeEnvironment.getApiLevel;
import static org.robolectric.Shadows.shadowOf;
+import android.app.PendingIntent;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.OnNetworkActiveListener;
import android.net.LinkProperties;
@@ -40,6 +41,8 @@ public class ShadowConnectivityManager {
private final Map<Integer, NetworkInfo> networkTypeToNetworkInfo = new HashMap<>();
private HashSet<ConnectivityManager.NetworkCallback> networkCallbacks = new HashSet<>();
+ private final HashSet<PendingIntent> networkCallbackPendingIntents = new HashSet<>();
+
private final Map<Integer, Network> netIdToNetwork = new HashMap<>();
private final Map<Integer, NetworkInfo> netIdToNetworkInfo = new HashMap<>();
private Network processBoundNetwork;
@@ -84,6 +87,10 @@ public class ShadowConnectivityManager {
return networkCallbacks;
}
+ public Set<PendingIntent> getNetworkCallbackPendingIntents() {
+ return networkCallbackPendingIntents;
+ }
+
/**
* @return networks and their connectivity status which was reported with {@link
* #reportNetworkConnectivity}.
@@ -106,6 +113,11 @@ public class ShadowConnectivityManager {
networkCallbacks.add(networkCallback);
}
+ @Implementation(minSdk = M)
+ protected void registerNetworkCallback(NetworkRequest request, PendingIntent pendingIntent) {
+ networkCallbackPendingIntents.add(pendingIntent);
+ }
+
@Implementation(minSdk = LOLLIPOP)
protected void requestNetwork(
NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
@@ -151,6 +163,16 @@ public class ShadowConnectivityManager {
}
}
+ @Implementation(minSdk = M)
+ protected void unregisterNetworkCallback(PendingIntent pendingIntent) {
+ if (pendingIntent == null) {
+ throw new IllegalArgumentException("Invalid NetworkCallback");
+ }
+ if (networkCallbackPendingIntents.contains(pendingIntent)) {
+ networkCallbackPendingIntents.remove(pendingIntent);
+ }
+ }
+
@Implementation
protected NetworkInfo getActiveNetworkInfo() {
return activeNetworkInfo;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowGeocoder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowGeocoder.java
index a218db760..03a05e9e6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowGeocoder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowGeocoder.java
@@ -1,7 +1,10 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
import android.location.Address;
import android.location.Geocoder;
+import android.location.Geocoder.GeocodeListener;
import com.google.common.base.Preconditions;
import java.io.IOException;
import java.util.ArrayList;
@@ -42,6 +45,29 @@ public final class ShadowGeocoder {
}
/**
+ * Sets an empty list by default, or the last value set by {@link #setFromLocation(List)} in the
+ * provided {@code listener}
+ *
+ * <p>{@code latitude} and {@code longitude} are ignored by this implementation, except to check
+ * that they are in appropriate bounds. {@code maxResults} determines the maximum number of
+ * addresses to return.
+ */
+ @Implementation(minSdk = TIRAMISU)
+ protected void getFromLocation(
+ double latitude, double longitude, int maxResults, GeocodeListener listener)
+ throws IOException {
+ Preconditions.checkArgument(
+ -90 <= latitude && latitude <= 90, "Latitude must be between -90 and 90, got %s", latitude);
+ Preconditions.checkArgument(
+ -180 <= longitude && longitude <= 180,
+ "Longitude must be between -180 and 180, got %s",
+ longitude);
+
+ // On real Android this callback will not happen synchronously.
+ listener.onGeocode(fromLocation.subList(0, Math.min(maxResults, fromLocation.size())));
+ }
+
+ /**
* Sets the value to be returned by {@link Geocoder#isPresent()}.
*
* <p>This value is reset to true for each test.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
index 15923c841..0654fbc4f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.S_V2;
import static android.os.Build.VERSION_CODES.TIRAMISU;
@@ -26,7 +27,7 @@ import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
/** Shadow for {@link android.media.ImageReader} */
-@Implements(ImageReader.class)
+@Implements(value = ImageReader.class, looseSignatures = true)
public class ShadowImageReader {
// Using same return codes as ImageReader.
private static final int ACQUIRE_SUCCESS = 0;
@@ -64,11 +65,16 @@ public class ShadowImageReader {
return ACQUIRE_SUCCESS;
}
- @Implementation(minSdk = TIRAMISU)
+ @Implementation(minSdk = TIRAMISU, maxSdk = TIRAMISU)
protected int nativeImageSetup(Image image, boolean useLegacyImageFormat) {
return nativeImageSetup(image);
}
+ @Implementation(minSdk = CUR_DEVELOPMENT)
+ protected int nativeImageSetup(Object /* Image */ image) {
+ return nativeImageSetup((Image) image);
+ }
+
@Implementation(minSdk = KITKAT)
protected void nativeReleaseImage(Image i) {
openedImages.remove(i);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
index ee25a81be..20a6d1364 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
@@ -8,6 +8,7 @@ import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.os.Bundle;
@@ -74,7 +75,7 @@ public class ShadowInputMethodManager {
return true;
}
- @Implementation(minSdk = S)
+ @Implementation(minSdk = S, maxSdk = TIRAMISU)
protected boolean showSoftInput(
View view, int flags, ResultReceiver resultReceiver, int ignoredReason) {
return showSoftInput(view, flags, resultReceiver);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobScheduler.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobScheduler.java
index 2a7fdf19a..1ecdd2340 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobScheduler.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobScheduler.java
@@ -13,6 +13,7 @@ import android.app.job.JobScheduler;
import android.app.job.JobWorkItem;
import android.os.Build;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -57,8 +58,9 @@ public abstract class ShadowJobScheduler {
@Implements(value = JobSchedulerImpl.class, isInAndroidSdk = false, minSdk = LOLLIPOP)
public static class ShadowJobSchedulerImpl extends ShadowJobScheduler {
- private Map<Integer, JobInfo> scheduledJobs = new LinkedHashMap<>();
- private Set<Integer> jobsToFail = new HashSet<>();
+ private final Map<Integer, JobInfo> scheduledJobs =
+ Collections.synchronizedMap(new LinkedHashMap<>());
+ private final Set<Integer> jobsToFail = Collections.synchronizedSet(new HashSet<>());
private boolean failExpeditedJobEnabled;
@Override
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyAssetManager.java
index 7a8a57c4d..33353358b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyAssetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyAssetManager.java
@@ -9,6 +9,7 @@ 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.TIRAMISU;
import static org.robolectric.RuntimeEnvironment.castNativePtr;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
@@ -697,7 +698,7 @@ public class ShadowLegacyAssetManager extends ShadowAssetManager {
}
@HiddenApi
- @Implementation(minSdk = VERSION_CODES.O)
+ @Implementation(minSdk = VERSION_CODES.O, maxSdk = TIRAMISU)
public void setConfiguration(
int mcc,
int mnc,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
index f07c7fadc..f624b60d5 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
@@ -62,12 +62,6 @@ public class ShadowLegacyLooper extends ShadowLooper {
// ignore if realistic looper
return;
}
- // Blech. We need to keep the main looper because somebody might refer to it in a static
- // field. The other loopers need to be wrapped in WeakReferences so that they are not prevented
- // from being garbage collected.
- if (!isMainThread()) {
- throw new IllegalStateException("you should only be calling this from the main thread!");
- }
synchronized (loopingLoopers) {
for (Looper looper : loopingLoopers.values()) {
synchronized (looper) {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLinux.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLinux.java
index 3053314c5..41a109c7e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLinux.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLinux.java
@@ -9,7 +9,9 @@ import android.system.StructStat;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.io.RandomAccessFile;
import java.time.Duration;
import libcore.io.Linux;
@@ -18,6 +20,7 @@ import org.robolectric.annotation.Implements;
@Implements(value = Linux.class, minSdk = Build.VERSION_CODES.O, isInAndroidSdk = false)
public class ShadowLinux {
+
@Implementation
public void mkdir(String path, int mode) throws ErrnoException {
new File(path).mkdirs();
@@ -76,6 +79,20 @@ public class ShadowLinux {
}
}
+ @Implementation
+ protected int pread(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, long offset)
+ throws ErrnoException, InterruptedIOException {
+
+ try (FileInputStream fis = new FileInputStream(fd)) {
+ for (long n = offset; n > 0; ) {
+ n -= fis.skip(n);
+ }
+ return fis.read(bytes, byteOffset, byteCount);
+ } catch (IOException e) {
+ return -1;
+ }
+ }
+
private static String modeToString(int mode) {
if (mode == OsConstants.O_RDONLY) {
return "r";
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocationManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocationManager.java
index 0d61eb6d5..9cbf5eefa 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocationManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLocationManager.java
@@ -38,6 +38,7 @@ import android.os.UserHandle;
import android.os.WorkSource;
import android.provider.Settings.Secure;
import android.text.TextUtils;
+import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -72,6 +73,8 @@ import org.robolectric.util.ReflectionHelpers.ClassParameter;
@Implements(value = LocationManager.class, looseSignatures = true)
public class ShadowLocationManager {
+ private static final String TAG = "ShadowLocationManager";
+
private static final long GET_CURRENT_LOCATION_TIMEOUT_MS = 30 * 1000;
private static final long MAX_CURRENT_LOCATION_AGE_MS = 10 * 1000;
@@ -310,6 +313,8 @@ public class ShadowLocationManager {
private int gnssYearOfHardware;
+ private int gnssBatchSize;
+
public ShadowLocationManager() {
// create default providers
providers.add(
@@ -882,6 +887,26 @@ public class ShadowLocationManager {
}
}
+ @Implementation(minSdk = VERSION_CODES.S)
+ protected void requestFlush(String provider, LocationListener listener, int requestCode) {
+ ProviderEntry entry = getProviderEntry(provider);
+ if (entry == null) {
+ throw new IllegalArgumentException("unknown provider \"" + provider + "\"");
+ }
+
+ entry.requestFlush(listener, requestCode);
+ }
+
+ @Implementation(minSdk = VERSION_CODES.S)
+ protected void requestFlush(String provider, PendingIntent pendingIntent, int requestCode) {
+ ProviderEntry entry = getProviderEntry(provider);
+ if (entry == null) {
+ throw new IllegalArgumentException("unknown provider \"" + provider + "\"");
+ }
+
+ entry.requestFlush(pendingIntent, requestCode);
+ }
+
/**
* Returns the list of {@link LocationRequest} currently registered under the given provider.
* Clients compiled against the public Android SDK should only use this method on S+, clients
@@ -924,6 +949,47 @@ public class ShadowLocationManager {
return false;
}
+ @Implementation(minSdk = VERSION_CODES.O)
+ protected int getGnssBatchSize() {
+ return gnssBatchSize;
+ }
+
+ /**
+ * Sets the GNSS hardware batch size. Values greater than 0 enables hardware GNSS batching APIs.
+ */
+ public void setGnssBatchSize(int gnssBatchSize) {
+ this.gnssBatchSize = gnssBatchSize;
+ }
+
+ @Implementation(minSdk = VERSION_CODES.O)
+ protected boolean registerGnssBatchedLocationCallback(
+ Object periodNanos, Object wakeOnFifoFull, Object callback, Object handler) {
+ getOrCreateProviderEntry(GPS_PROVIDER)
+ .setLegacyBatchedListener(
+ callback,
+ new HandlerExecutor((Handler) handler),
+ gnssBatchSize,
+ (Boolean) wakeOnFifoFull);
+ return true;
+ }
+
+ @Implementation(minSdk = VERSION_CODES.O)
+ protected void flushGnssBatch() {
+ ProviderEntry e = getProviderEntry(GPS_PROVIDER);
+ if (e != null) {
+ e.flushLegacyBatch();
+ }
+ }
+
+ @Implementation(minSdk = VERSION_CODES.O)
+ protected boolean unregisterGnssBatchedLocationCallback(Object callback) {
+ ProviderEntry e = getProviderEntry(GPS_PROVIDER);
+ if (e != null) {
+ e.clearLegacyBatchedListener();
+ }
+ return true;
+ }
+
@Implementation(minSdk = VERSION_CODES.P)
@Nullable
protected String getGnssHardwareModelName() {
@@ -1119,11 +1185,23 @@ public class ShadowLocationManager {
@Implementation(minSdk = VERSION_CODES.N)
protected boolean registerGnssMeasurementsCallback(
GnssMeasurementsEvent.Callback listener, Handler handler) {
- if (handler == null) {
- handler = new Handler();
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.R) {
+ if (handler == null) {
+ handler = new Handler();
+ }
+
+ return registerGnssMeasurementsCallback(new HandlerExecutor(handler), listener);
+ } else {
+ return registerGnssMeasurementsCallback(Runnable::run, listener);
}
+ }
- return registerGnssMeasurementsCallback(new HandlerExecutor(handler), listener);
+ @Implementation(minSdk = VERSION_CODES.R)
+ @RequiresApi(api = VERSION_CODES.R)
+ protected boolean registerGnssMeasurementsCallback(
+ Object request, Object executor, Object callback) {
+ return registerGnssMeasurementsCallback(
+ (Executor) executor, (GnssMeasurementsEvent.Callback) callback);
}
@Implementation(minSdk = VERSION_CODES.R)
@@ -1238,15 +1316,15 @@ public class ShadowLocationManager {
*
* <p>The location will also be delivered to the passive provider.
*/
- public void simulateLocation(String provider, Location location) {
+ public void simulateLocation(String provider, Location... locations) {
ProviderEntry providerEntry = getOrCreateProviderEntry(provider);
if (!PASSIVE_PROVIDER.equals(providerEntry.getName())) {
- providerEntry.simulateLocation(location);
+ providerEntry.simulateLocation(locations);
}
ProviderEntry passiveProviderEntry = getProviderEntry(PASSIVE_PROVIDER);
if (passiveProviderEntry != null) {
- passiveProviderEntry.simulateLocation(location);
+ passiveProviderEntry.simulateLocation(locations);
}
}
@@ -1411,6 +1489,10 @@ public class ShadowLocationManager {
@GuardedBy("this")
@Nullable
+ private LegacyBatchedTransport legacyBatchedTransport;
+
+ @GuardedBy("this")
+ @Nullable
private ProviderProperties properties;
@GuardedBy("this")
@@ -1587,15 +1669,21 @@ public class ShadowLocationManager {
lastLocation = location;
}
- public void simulateLocation(Location location) {
+ public void simulateLocation(Location... locations) {
List<LocationTransport<?>> transports;
+ LegacyBatchedTransport batchedTransport;
synchronized (this) {
- lastLocation = new Location(location);
+ lastLocation = new Location(locations[locations.length - 1]);
transports = locationTransports;
+ batchedTransport = legacyBatchedTransport;
+ }
+
+ if (batchedTransport != null) {
+ batchedTransport.invokeOnLocations(locations);
}
for (LocationTransport<?> transport : transports) {
- if (!transport.invokeOnLocation(location)) {
+ if (!transport.invokeOnLocations(locations)) {
synchronized (this) {
Iterables.removeIf(locationTransports, current -> current == transport);
}
@@ -1623,6 +1711,31 @@ public class ShadowLocationManager {
addListenerInternal(new LocationPendingIntentTransport(getContext(), pendingIntent, request));
}
+ public void setLegacyBatchedListener(
+ Object callback, Executor executor, int batchSize, boolean flushOnFifoFull) {
+ synchronized (this) {
+ legacyBatchedTransport =
+ new LegacyBatchedTransport(callback, executor, batchSize, flushOnFifoFull);
+ }
+ }
+
+ public void flushLegacyBatch() {
+ LegacyBatchedTransport batchedTransport;
+ synchronized (this) {
+ batchedTransport = legacyBatchedTransport;
+ }
+
+ if (batchedTransport != null) {
+ batchedTransport.invokeFlush();
+ }
+ }
+
+ public void clearLegacyBatchedListener() {
+ synchronized (this) {
+ legacyBatchedTransport = null;
+ }
+ }
+
private void addListenerInternal(LocationTransport<?> transport) {
boolean invokeOnProviderEnabled;
synchronized (this) {
@@ -1644,6 +1757,23 @@ public class ShadowLocationManager {
Iterables.removeIf(locationTransports, transport -> transport.getKey() == key);
}
+ public void requestFlush(Object key, int requestCode) {
+ LocationTransport<?> transport;
+ synchronized (this) {
+ transport = Iterables.tryFind(locationTransports, t -> t.getKey() == key).orNull();
+ }
+
+ if (transport == null) {
+ throw new IllegalArgumentException("unregistered listener cannot be flushed");
+ }
+
+ if (!transport.invokeOnFlush(requestCode)) {
+ synchronized (this) {
+ Iterables.removeIf(locationTransports, current -> current == transport);
+ }
+ }
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof ProviderEntry) {
@@ -1846,28 +1976,43 @@ public class ShadowLocationManager {
}
// return false if this listener should be removed by this invocation
- public boolean invokeOnLocation(Location location) {
- if (lastDeliveredLocation != null) {
- if (location.getTime() - lastDeliveredLocation.getTime()
- < request.getMinUpdateIntervalMillis()) {
- return true;
- }
- if (distanceBetween(location, lastDeliveredLocation)
- < request.getMinUpdateDistanceMeters()) {
- return true;
+ public boolean invokeOnLocations(Location... locations) {
+ ArrayList<Location> deliverableLocations = new ArrayList<>(locations.length);
+ for (Location location : locations) {
+ if (lastDeliveredLocation != null) {
+ if (location.getTime() - lastDeliveredLocation.getTime()
+ < request.getMinUpdateIntervalMillis()) {
+ Log.w(TAG, "location rejected for simulated delivery - too fast");
+ continue;
+ }
+ if (distanceBetween(location, lastDeliveredLocation)
+ < request.getMinUpdateDistanceMeters()) {
+ Log.w(TAG, "location rejected for simulated delivery - too close");
+ continue;
+ }
}
+
+ deliverableLocations.add(new Location(location));
+ lastDeliveredLocation = new Location(location);
}
- lastDeliveredLocation = new Location(location);
+ if (deliverableLocations.isEmpty()) {
+ return true;
+ }
boolean needsRemoval = false;
- if (++numDeliveries >= request.getMaxUpdates()) {
+ numDeliveries += deliverableLocations.size();
+ if (numDeliveries >= request.getMaxUpdates()) {
needsRemoval = true;
}
try {
- onLocation(location);
+ if (deliverableLocations.size() == 1) {
+ onLocation(deliverableLocations.get(0));
+ } else {
+ onLocations(deliverableLocations);
+ }
} catch (CanceledException e) {
needsRemoval = true;
}
@@ -1885,9 +2030,23 @@ public class ShadowLocationManager {
}
}
+ // return false if this listener should be removed by this invocation
+ public boolean invokeOnFlush(int requestCode) {
+ try {
+ onFlushComplete(requestCode);
+ return true;
+ } catch (CanceledException e) {
+ return false;
+ }
+ }
+
abstract void onLocation(Location location) throws CanceledException;
+ abstract void onLocations(List<Location> locations) throws CanceledException;
+
abstract void onProviderEnabled(String provider, boolean enabled) throws CanceledException;
+
+ abstract void onFlushComplete(int requestCode) throws CanceledException;
}
private static final class LocationListenerTransport extends LocationTransport<LocationListener> {
@@ -1901,12 +2060,26 @@ public class ShadowLocationManager {
}
@Override
- public void onLocation(Location location) {
- executor.execute(() -> getKey().onLocationChanged(new Location(location)));
+ void onLocation(Location location) {
+ executor.execute(() -> getKey().onLocationChanged(location));
+ }
+
+ @Override
+ void onLocations(List<Location> locations) {
+ executor.execute(
+ () -> {
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) {
+ getKey().onLocationChanged(locations);
+ } else {
+ for (Location location : locations) {
+ getKey().onLocationChanged(location);
+ }
+ }
+ });
}
@Override
- public void onProviderEnabled(String provider, boolean enabled) {
+ void onProviderEnabled(String provider, boolean enabled) {
executor.execute(
() -> {
if (enabled) {
@@ -1916,6 +2089,11 @@ public class ShadowLocationManager {
}
});
}
+
+ @Override
+ void onFlushComplete(int requestCode) {
+ executor.execute(() -> getKey().onFlushComplete(requestCode));
+ }
}
private static final class LocationPendingIntentTransport
@@ -1930,18 +2108,84 @@ public class ShadowLocationManager {
}
@Override
- public void onLocation(Location location) throws CanceledException {
+ void onLocation(Location location) throws CanceledException {
Intent intent = new Intent();
intent.putExtra(LocationManager.KEY_LOCATION_CHANGED, new Location(location));
getKey().send(context, 0, intent);
}
@Override
- public void onProviderEnabled(String provider, boolean enabled) throws CanceledException {
+ void onLocations(List<Location> locations) throws CanceledException {
+ if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.S) {
+ Intent intent = new Intent();
+ intent.putExtra(LocationManager.KEY_LOCATION_CHANGED, locations.get(locations.size() - 1));
+ intent.putExtra(LocationManager.KEY_LOCATIONS, locations.toArray(new Location[0]));
+ getKey().send(context, 0, intent);
+ } else {
+ for (Location location : locations) {
+ onLocation(location);
+ }
+ }
+ }
+
+ @Override
+ void onProviderEnabled(String provider, boolean enabled) throws CanceledException {
Intent intent = new Intent();
intent.putExtra(LocationManager.KEY_PROVIDER_ENABLED, enabled);
getKey().send(context, 0, intent);
}
+
+ @Override
+ void onFlushComplete(int requestCode) throws CanceledException {
+ Intent intent = new Intent();
+ intent.putExtra(LocationManager.KEY_FLUSH_COMPLETE, requestCode);
+ getKey().send(context, 0, intent);
+ }
+ }
+
+ private static final class LegacyBatchedTransport {
+
+ private final android.location.BatchedLocationCallback callback;
+ private final Executor executor;
+ private final int batchSize;
+ private final boolean flushOnFifoFull;
+
+ private ArrayList<Location> batch = new ArrayList<>();
+
+ LegacyBatchedTransport(
+ Object callback, Executor executor, int batchSize, boolean flushOnFifoFull) {
+ this.callback = (android.location.BatchedLocationCallback) callback;
+ this.executor = executor;
+ this.batchSize = batchSize;
+ this.flushOnFifoFull = flushOnFifoFull;
+ }
+
+ public void invokeFlush() {
+ ArrayList<Location> delivery = batch;
+ batch = new ArrayList<>();
+ executor.execute(
+ () -> {
+ callback.onLocationBatch(delivery);
+ if (!delivery.isEmpty()) {
+ callback.onLocationBatch(new ArrayList<>());
+ }
+ });
+ }
+
+ public void invokeOnLocations(Location... locations) {
+ for (Location location : locations) {
+ batch.add(new Location(location));
+ if (batch.size() >= batchSize) {
+ if (!flushOnFifoFull) {
+ batch.remove(0);
+ } else {
+ ArrayList<Location> delivery = batch;
+ batch = new ArrayList<>();
+ executor.execute(() -> callback.onLocationBatch(delivery));
+ }
+ }
+ }
+ }
}
/**
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
index f6481f518..103907b93 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
@@ -1,9 +1,11 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.N_MR1;
import static android.os.Build.VERSION_CODES.O;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static org.robolectric.shadow.api.Shadow.invokeConstructor;
@@ -404,22 +406,32 @@ public class ShadowMediaCodec {
}
/** Prevents calling Android-only methods on basic ByteBuffer objects. */
- @Implementation(minSdk = LOLLIPOP)
+ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
+ @Implementation(minSdk = CUR_DEVELOPMENT)
+ protected void invalidateByteBufferLocked(
+ @Nullable ByteBuffer[] buffers, int index, boolean input) {}
+
/** Prevents calling Android-only methods on basic ByteBuffer objects. */
- @Implementation(minSdk = LOLLIPOP)
+ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
+ @Implementation(minSdk = CUR_DEVELOPMENT)
+ protected void validateInputByteBufferLocked(@Nullable ByteBuffer[] buffers, int index) {}
+
/** Prevents calling Android-only methods on basic ByteBuffer objects. */
- @Implementation(minSdk = LOLLIPOP)
+ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
+ @Implementation(minSdk = CUR_DEVELOPMENT)
+ protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index, boolean input) {}
+
/**
* Prevents calling Android-only methods on basic ByteBuffer objects. Replicates existing behavior
* adjusting buffer positions and limits.
*/
- @Implementation(minSdk = LOLLIPOP)
+ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void validateOutputByteBuffer(
@Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
if (buffers != null && index >= 0 && index < buffers.length) {
@@ -430,14 +442,26 @@ public class ShadowMediaCodec {
}
}
+ @Implementation(minSdk = CUR_DEVELOPMENT)
+ protected void validateOutputByteBufferLocked(
+ @Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
+ validateOutputByteBuffer(buffers, index, info);
+ }
+
/** Prevents calling Android-only methods on basic ByteBuffer objects. */
- @Implementation(minSdk = LOLLIPOP)
+ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {}
+ @Implementation(minSdk = CUR_DEVELOPMENT)
+ protected void invalidateByteBuffersLocked(@Nullable ByteBuffer[] buffers) {}
+
/** Prevents attempting to free non-direct ByteBuffer objects. */
- @Implementation(minSdk = LOLLIPOP)
+ @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
protected void freeByteBuffer(@Nullable ByteBuffer buffer) {}
+ @Implementation(minSdk = CUR_DEVELOPMENT)
+ protected void freeByteBufferLocked(@Nullable ByteBuffer buffer) {}
+
/** Shadows CodecBuffer to prevent attempting to free non-direct ByteBuffer objects. */
@Implements(className = "android.media.MediaCodec$BufferMap$CodecBuffer", minSdk = LOLLIPOP)
protected static class ShadowCodecBuffer {
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMemoryMappedFileS.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMemoryMappedFileS.java
index 79920d29a..f7929a55c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMemoryMappedFileS.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMemoryMappedFileS.java
@@ -22,10 +22,14 @@ public class ShadowMemoryMappedFileS {
private static final String TZ_DATA_1 = "/misc/zoneinfo/tzdata";
private static final String TZ_DATA_2 = "/usr/share/zoneinfo/tzdata";
private static final String TZ_DATA_3 = "/misc/zoneinfo/current/tzdata";
+ private static final String TZ_DATA_4 = "/etc/tz/tzdata";
@Implementation
public static MemoryMappedFile mmapRO(String path) throws Throwable {
- if (path.endsWith(TZ_DATA_1) || path.endsWith(TZ_DATA_2) || path.endsWith(TZ_DATA_3)) {
+ if (path.endsWith(TZ_DATA_1)
+ || path.endsWith(TZ_DATA_2)
+ || path.endsWith(TZ_DATA_3)
+ || path.endsWith(TZ_DATA_4)) {
InputStream is = MemoryMappedFile.class.getResourceAsStream(TZ_DATA_2);
if (is == null) {
throw new ErrnoException("open", -1);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
index 260b2171c..8b7b8fa86 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.TIRAMISU;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java
index 263522d84..2a1b2827b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeHardwareRenderer.java
@@ -137,8 +137,9 @@ public class ShadowNativeHardwareRenderer {
}
@Implementation(minSdk = S)
- protected static void nSetColorMode(long nativeProxy, int colorMode) {
+ protected static Object nSetColorMode(long nativeProxy, int colorMode) {
HardwareRendererNatives.nSetColorMode(nativeProxy, colorMode);
+ return null;
}
@Implementation(minSdk = S)
@@ -377,7 +378,8 @@ public class ShadowNativeHardwareRenderer {
int wideColorDataspace,
long appVsyncOffsetNanos,
long presentationDeadlineNanos,
- boolean supportsFp16ForHdr) {
+ boolean supportsFp16ForHdr,
+ boolean nInitDisplayInfo) {
nInitDisplayInfo(
width,
height,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java
index 2f91ba9c2..93f357487 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java
@@ -1,5 +1,7 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
import android.opengl.Matrix;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -25,7 +27,7 @@ public class ShadowOpenGLMatrix {
* @throws IllegalArgumentException if result, lhs, or rhs are null, or if resultOffset + 16 >
* result.length or lhsOffset + 16 > lhs.length or rhsOffset + 16 > rhs.length.
*/
- @Implementation
+ @Implementation(maxSdk = TIRAMISU)
protected static void multiplyMM(
float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset) {
if (result == null) {
@@ -84,7 +86,7 @@ public class ShadowOpenGLMatrix {
* resultVecOffset + 4 > resultVec.length or lhsMatOffset + 16 > lhsMat.length or rhsVecOffset
* + 4 > rhsVec.length.
*/
- @Implementation
+ @Implementation(maxSdk = TIRAMISU)
protected static void multiplyMV(
float[] resultVec,
int resultVecOffset,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
index 8ddcc7bc2..162330aad 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
@@ -56,7 +56,7 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
int ptr = (int) nativeQueueRegistry.register(this);
reflector(MessageQueueReflector.class, realQueue).setPtr(ptr);
clockListener = () -> nativeWake(ptr);
- ShadowPausedSystemClock.addListener(clockListener);
+ ShadowPausedSystemClock.addStaticListener(clockListener);
}
@Implementation(maxSdk = JELLY_BEAN_MR1)
@@ -234,6 +234,12 @@ public class ShadowPausedMessageQueue extends ShadowMessageQueue {
}
}
+ @Implementation(minSdk = JELLY_BEAN_MR2)
+ protected void quit(boolean allowed) {
+ reflector(MessageQueueReflector.class, realQueue).quit(allowed);
+ ShadowPausedSystemClock.removeListener(clockListener);
+ }
+
private boolean isQuitting() {
if (RuntimeEnvironment.getApiLevel() >= KITKAT) {
return reflector(MessageQueueReflector.class, realQueue).getQuitting();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedSystemClock.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedSystemClock.java
index 9d583e3bf..193c08615 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedSystemClock.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedSystemClock.java
@@ -38,6 +38,9 @@ public class ShadowPausedSystemClock extends ShadowSystemClock {
private static long currentTimeMillis = INITIAL_TIME;
private static final List<Listener> listeners = new CopyOnWriteArrayList<>();
+ // hopefully temporary list of clock listeners that are NOT cleared between tests
+ // This is needed to accomodate Loopers which are not reset between tests
+ private static final List<Listener> staticListeners = new CopyOnWriteArrayList<>();
/**
* Callback for clock updates
@@ -52,6 +55,11 @@ public class ShadowPausedSystemClock extends ShadowSystemClock {
static void removeListener(Listener listener) {
listeners.remove(listener);
+ staticListeners.remove(listener);
+ }
+
+ static void addStaticListener(Listener listener) {
+ staticListeners.add(listener);
}
/** Advances the current time by given millis, without sleeping the current thread/ */
@@ -60,9 +68,16 @@ public class ShadowPausedSystemClock extends ShadowSystemClock {
synchronized (ShadowPausedSystemClock.class) {
currentTimeMillis += millis;
}
+ informListeners();
+ }
+
+ private static void informListeners() {
for (Listener listener : listeners) {
listener.onClockAdvanced();
}
+ for (Listener listener : staticListeners) {
+ listener.onClockAdvanced();
+ }
}
/**
@@ -83,9 +98,7 @@ public class ShadowPausedSystemClock extends ShadowSystemClock {
currentTimeMillis = millis;
}
}
- for (Listener listener : listeners) {
- listener.onClockAdvanced();
- }
+ informListeners();
return true;
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
index cf06d40d6..a1895ff6b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
@@ -1,9 +1,11 @@
package org.robolectric.shadows;
+import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.N_MR1;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
import android.content.Context;
import android.media.IAudioService;
@@ -54,13 +56,25 @@ public class ShadowSoundPool {
return 1;
}
- @Implementation(minSdk = M)
+ @Implementation(minSdk = M, maxSdk = TIRAMISU)
protected int _play(
int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate) {
playedSounds.add(new Playback(soundID, leftVolume, rightVolume, priority, loop, rate));
return 1;
}
+ @Implementation(minSdk = CUR_DEVELOPMENT)
+ protected int _play(
+ int soundID,
+ float leftVolume,
+ float rightVolume,
+ int priority,
+ int loop,
+ float rate,
+ int playerIId /* ignored */) {
+ return _play(soundID, leftVolume, rightVolume, priority, loop, rate);
+ }
+
// It's not possible to override the native _load method as that would only give access to a
// FileDescriptor which would make it difficult to check if a given sound has been placed.
@Implementation
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java
index 9287cbb0f..4f60510a7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSpeechRecognizer.java
@@ -38,6 +38,7 @@ public class ShadowSpeechRecognizer {
private Intent recognizerIntent;
private RecognitionListener recognitionListener;
private static boolean isOnDeviceRecognitionAvailable = true;
+ private boolean isRecognizerDestroyed = false;
private /*RecognitionSupportCallback*/ Object recognitionSupportCallback;
private Executor recognitionSupportExecutor;
@@ -56,6 +57,11 @@ public class ShadowSpeechRecognizer {
return recognizerIntent;
}
+ /** Returns true iff the destroy method of was invoked for the recognizer. */
+ public boolean isDestroyed() {
+ return isRecognizerDestroyed;
+ }
+
@Resetter
public static void reset() {
latestSpeechRecognizer = null;
@@ -63,6 +69,12 @@ public class ShadowSpeechRecognizer {
}
@Implementation
+ protected void destroy() {
+ isRecognizerDestroyed = true;
+ reflector(SpeechRecognizerReflector.class, realSpeechRecognizer).destroy();
+ }
+
+ @Implementation
protected static SpeechRecognizer createSpeechRecognizer(
final Context context, final ComponentName serviceComponent) {
SpeechRecognizer result =
@@ -75,6 +87,10 @@ public class ShadowSpeechRecognizer {
@Implementation
protected void startListening(Intent recognizerIntent) {
this.recognizerIntent = recognizerIntent;
+ // from the implementation of {@link SpeechRecognizer#startListening} it seems that it allows
+ // running the method on an already destroyed object, so we replicate the same by resetting
+ // isRecognizerDestroyed
+ isRecognizerDestroyed = false;
// the real implementation connects to a service
// simulate the resulting behavior once the service is connected
Handler mainHandler = new Handler(Looper.getMainLooper());
@@ -192,6 +208,9 @@ public class ShadowSpeechRecognizer {
@Direct
SpeechRecognizer createSpeechRecognizer(Context context, ComponentName serviceComponent);
+ @Direct
+ void destroy();
+
@Accessor("mService")
void setService(IRecognitionService service);
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 902669fbc..0864354d9 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -1,5 +1,6 @@
package org.robolectric.shadows;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
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;
@@ -20,7 +21,9 @@ import static android.telephony.PhoneStateListener.LISTEN_SERVICE_STATE;
import static android.telephony.TelephonyManager.CALL_STATE_IDLE;
import static android.telephony.TelephonyManager.CALL_STATE_RINGING;
+import android.Manifest.permission;
import android.annotation.CallSuper;
+import android.app.ActivityThread;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -64,11 +67,13 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
+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.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
@Implements(value = TelephonyManager.class, looseSignatures = true)
@@ -146,6 +151,7 @@ public class ShadowTelephonyManager {
private static int callComposerStatus = 0;
private VisualVoicemailSmsParams lastVisualVoicemailSmsParams;
private VisualVoicemailSmsFilterSettings visualVoicemailSmsFilterSettings;
+ private boolean emergencyCallbackMode;
/**
* Should be {@link TelephonyManager.BootstrapAuthenticationCallback} but this object was
@@ -524,6 +530,23 @@ public class ShadowTelephonyManager {
}
}
+ private void checkReadPrivilegedPhoneStatePermission() {
+ if (!checkPermission(permission.READ_PRIVILEGED_PHONE_STATE)) {
+ throw new SecurityException();
+ }
+ }
+
+ static ShadowInstrumentation getShadowInstrumentation() {
+ ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
+ return Shadow.extract(activityThread.getInstrumentation());
+ }
+
+ static boolean checkPermission(String permission) {
+ return getShadowInstrumentation()
+ .checkPermission(permission, android.os.Process.myPid(), android.os.Process.myUid())
+ == PERMISSION_GRANTED;
+ }
+
@Implementation
protected int getPhoneType() {
return phoneType;
@@ -1159,6 +1182,22 @@ public class ShadowTelephonyManager {
return false;
}
+ /**
+ * Emergency Callback Mode (ECBM) is typically set by the carrier, for a time window of 5 minutes
+ * after the last outgoing emergency call. The user can exit ECBM via a system notification.
+ *
+ * @param emergencyCallbackMode whether the device is in ECBM or not.
+ */
+ public void setEmergencyCallbackMode(boolean emergencyCallbackMode) {
+ this.emergencyCallbackMode = emergencyCallbackMode;
+ }
+
+ @Implementation(minSdk = Build.VERSION_CODES.O)
+ protected boolean getEmergencyCallbackMode() {
+ checkReadPrivilegedPhoneStatePermission();
+ return emergencyCallbackMode;
+ }
+
@Implementation(minSdk = Build.VERSION_CODES.Q)
protected boolean isPotentialEmergencyNumber(String number) {
return isEmergencyNumber(number);
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 561ed907f..8e933d93e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
@@ -12,6 +12,7 @@ import android.net.ConnectivityManager;
import android.net.DhcpInfo;
import android.net.NetworkInfo;
import android.net.wifi.ScanResult;
+import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
@@ -53,7 +54,8 @@ public class ShadowWifiManager {
private boolean wasSaved = false;
private WifiInfo wifiInfo;
private List<ScanResult> scanResults;
- private final Map<Integer, WifiConfiguration> networkIdToConfiguredNetworks = new LinkedHashMap<>();
+ private final Map<Integer, WifiConfiguration> networkIdToConfiguredNetworks =
+ new LinkedHashMap<>();
private Pair<Integer, Boolean> lastEnabledNetwork;
private final Set<Integer> enabledNetworks = new HashSet<>();
private DhcpInfo dhcpInfo;
@@ -68,6 +70,7 @@ public class ShadowWifiManager {
private Object networkScorer;
@RealObject WifiManager wifiManager;
private WifiConfiguration apConfig;
+ private SoftApConfiguration softApConfig;
@Implementation
protected boolean setWifiEnabled(boolean wifiEnabled) {
@@ -123,9 +126,7 @@ public class ShadowWifiManager {
this.isStaApConcurrencySupported = isStaApConcurrencySupported;
}
- /**
- * Sets the connection info as the provided {@link WifiInfo}.
- */
+ /** Sets the connection info as the provided {@link WifiInfo}. */
public void setConnectionInfo(WifiInfo wifiInfo) {
this.wifiInfo = wifiInfo;
}
@@ -267,9 +268,10 @@ public class ShadowWifiManager {
protected void connect(WifiConfiguration wifiConfiguration, WifiManager.ActionListener listener) {
WifiInfo wifiInfo = getConnectionInfo();
- String ssid = isQuoted(wifiConfiguration.SSID)
- ? stripQuotes(wifiConfiguration.SSID)
- : wifiConfiguration.SSID;
+ String ssid =
+ isQuoted(wifiConfiguration.SSID)
+ ? stripQuotes(wifiConfiguration.SSID)
+ : wifiConfiguration.SSID;
ShadowWifiInfo shadowWifiInfo = Shadow.extract(wifiInfo);
shadowWifiInfo.setSSID(ssid);
@@ -474,6 +476,17 @@ public class ShadowWifiManager {
return apConfig;
}
+ @Implementation(minSdk = R)
+ protected boolean setSoftApConfiguration(SoftApConfiguration softApConfig) {
+ this.softApConfig = softApConfig;
+ return true;
+ }
+
+ @Implementation(minSdk = R)
+ protected SoftApConfiguration getSoftApConfiguration() {
+ return softApConfig;
+ }
+
/**
* Returns wifi usability scores previous passed to {@link WifiManager#updateWifiUsabilityScore}
*/
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/SharedLibraryInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/SharedLibraryInfoBuilder.java
new file mode 100644
index 000000000..896abaad7
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/SharedLibraryInfoBuilder.java
@@ -0,0 +1,125 @@
+package org.robolectric.shadows;
+
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.VersionedPackage;
+import android.os.Build.VERSION_CODES;
+import java.util.List;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Builder for {@link SharedLibraryInfo}. */
+public final class SharedLibraryInfoBuilder {
+ private String path;
+ private String packageName;
+ private List<String> codePaths;
+ private String name;
+ private long version;
+ private int type;
+ private VersionedPackage declaringPackage;
+ private List<VersionedPackage> dependentPackages;
+ private List<SharedLibraryInfo> dependencies;
+ private boolean isNative;
+
+ private SharedLibraryInfoBuilder() {}
+
+ public static SharedLibraryInfoBuilder newBuilder() {
+ return new SharedLibraryInfoBuilder();
+ }
+
+ public SharedLibraryInfoBuilder setPath(String path) {
+ this.path = path;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setPackageName(String packageName) {
+ this.packageName = packageName;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setCodePaths(List<String> codePaths) {
+ this.codePaths = codePaths;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setVersion(long version) {
+ this.version = version;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setType(int type) {
+ this.type = type;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setDeclaringPackage(VersionedPackage declaringPackage) {
+ this.declaringPackage = declaringPackage;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setDependentPackages(List<VersionedPackage> dependentPackages) {
+ this.dependentPackages = dependentPackages;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setDependencies(List<SharedLibraryInfo> dependencies) {
+ this.dependencies = dependencies;
+ return this;
+ }
+
+ public SharedLibraryInfoBuilder setIsNative(boolean isNative) {
+ this.isNative = isNative;
+ return this;
+ }
+
+ public SharedLibraryInfo build() {
+ int apiLevel = RuntimeEnvironment.getApiLevel();
+ if (apiLevel <= VERSION_CODES.O_MR1) {
+ return ReflectionHelpers.callConstructor(
+ SharedLibraryInfo.class,
+ ClassParameter.from(String.class, name),
+ ClassParameter.from(int.class, (int) version),
+ ClassParameter.from(int.class, type),
+ ClassParameter.from(VersionedPackage.class, declaringPackage),
+ ClassParameter.from(List.class, dependentPackages));
+ } else if (apiLevel <= VERSION_CODES.P) {
+ return ReflectionHelpers.callConstructor(
+ SharedLibraryInfo.class,
+ ClassParameter.from(String.class, name),
+ ClassParameter.from(long.class, version),
+ ClassParameter.from(int.class, type),
+ ClassParameter.from(VersionedPackage.class, declaringPackage),
+ ClassParameter.from(List.class, dependentPackages));
+ } else if (apiLevel <= VERSION_CODES.R) {
+ return ReflectionHelpers.callConstructor(
+ SharedLibraryInfo.class,
+ ClassParameter.from(String.class, path),
+ ClassParameter.from(String.class, packageName),
+ ClassParameter.from(List.class, codePaths),
+ ClassParameter.from(String.class, name),
+ ClassParameter.from(long.class, version),
+ ClassParameter.from(int.class, type),
+ ClassParameter.from(VersionedPackage.class, declaringPackage),
+ ClassParameter.from(List.class, dependentPackages),
+ ClassParameter.from(List.class, dependencies));
+ } else {
+ return ReflectionHelpers.callConstructor(
+ SharedLibraryInfo.class,
+ ClassParameter.from(String.class, path),
+ ClassParameter.from(String.class, packageName),
+ ClassParameter.from(List.class, codePaths),
+ ClassParameter.from(String.class, name),
+ ClassParameter.from(long.class, version),
+ ClassParameter.from(int.class, type),
+ ClassParameter.from(VersionedPackage.class, declaringPackage),
+ ClassParameter.from(List.class, dependentPackages),
+ ClassParameter.from(List.class, dependencies),
+ ClassParameter.from(boolean.class, isNative));
+ }
+ }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
index cbb6e785f..fc63fc90c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/WifiUsabilityStatsEntryBuilder.java
@@ -5,6 +5,7 @@ import android.net.wifi.WifiUsabilityStatsEntry.ContentionTimeStats;
import android.net.wifi.WifiUsabilityStatsEntry.RadioStats;
import android.net.wifi.WifiUsabilityStatsEntry.RateStats;
import android.os.Build.VERSION_CODES;
+import android.util.SparseArray;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
@@ -75,6 +76,46 @@ public class WifiUsabilityStatsEntryBuilder {
ClassParameter.from(int.class, cellularSignalStrengthDbm),
ClassParameter.from(int.class, cellularSignalStrengthDb),
ClassParameter.from(boolean.class, isSameRegisteredCell));
+ } else if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) {
+ return ReflectionHelpers.callConstructor(
+ WifiUsabilityStatsEntry.class,
+ ClassParameter.from(long.class, timeStampMillis),
+ ClassParameter.from(int.class, rssi),
+ ClassParameter.from(int.class, linkSpeedMbps),
+ ClassParameter.from(long.class, totalTxSuccess),
+ ClassParameter.from(long.class, totalTxRetries),
+ ClassParameter.from(long.class, totalTxBad),
+ ClassParameter.from(long.class, totalRxSuccess),
+ ClassParameter.from(long.class, totalRadioOnTimeMillis),
+ ClassParameter.from(long.class, totalRadioTxTimeMillis),
+ ClassParameter.from(long.class, totalRadioRxTimeMillis),
+ ClassParameter.from(long.class, totalScanTimeMillis),
+ ClassParameter.from(long.class, totalNanScanTimeMillis),
+ ClassParameter.from(long.class, totalBackgroundScanTimeMillis),
+ ClassParameter.from(long.class, totalRoamScanTimeMillis),
+ ClassParameter.from(long.class, totalPnoScanTimeMillis),
+ ClassParameter.from(long.class, totalHotspot2ScanTimeMillis),
+ ClassParameter.from(long.class, totalCcaBusyFreqTimeMillis),
+ ClassParameter.from(long.class, totalRadioOnFreqTimeMillis),
+ ClassParameter.from(long.class, totalBeaconRx),
+ ClassParameter.from(int.class, probeStatusSinceLastUpdate),
+ ClassParameter.from(int.class, probeElapsedTimeSinceLastUpdateMillis),
+ ClassParameter.from(int.class, probeMcsRateSinceLastUpdate),
+ ClassParameter.from(int.class, rxLinkSpeedMbps),
+ ClassParameter.from(int.class, timeSliceDutyCycleInPercent), // new in T
+ ClassParameter.from(
+ ContentionTimeStats[].class, new ContentionTimeStats[] {}), // new in T
+ ClassParameter.from(RateStats[].class, new RateStats[] {}), // new in T
+ ClassParameter.from(RadioStats[].class, new RadioStats[] {}), // new in T
+ ClassParameter.from(int.class, CHANNEL_UTILIZATION_RATIO), // new in T
+ ClassParameter.from(boolean.class, isThroughputSufficient), // new in T
+ ClassParameter.from(boolean.class, isWifiScoringEnabled), // new in T
+ ClassParameter.from(boolean.class, isCellularDataAvailable), // new in T
+ ClassParameter.from(int.class, cellularDataNetworkType),
+ ClassParameter.from(int.class, cellularSignalStrengthDbm),
+ ClassParameter.from(int.class, cellularSignalStrengthDb),
+ ClassParameter.from(boolean.class, isSameRegisteredCell),
+ ClassParameter.from(SparseArray.class, new SparseArray<>())); // new in >T
} else {
return new WifiUsabilityStatsEntry(
timeStampMillis,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java b/shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java
index e2eb442de..f6ab51296 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/_Activity_.java
@@ -322,6 +322,8 @@ public interface _Activity_ {
void performRestart(boolean start, String reason);
+ void performRestart(boolean start);
+
void performRestoreInstanceState(Bundle savedInstanceState);
void performResume();
diff --git a/utils/build.gradle b/utils/build.gradle
index 4bd030f1f..c10cca279 100644
--- a/utils/build.gradle
+++ b/utils/build.gradle
@@ -9,7 +9,7 @@ apply plugin: "com.diffplug.spotless"
spotless {
kotlin {
target '**/*.kt'
- ktfmt('0.34').googleStyle()
+ ktfmt('0.42').googleStyle()
}
}
@@ -26,9 +26,7 @@ compileKotlin {
// in production. If utils module starts to add Kotlin code in main source
// set, we can remove this destinationDirectory modification.
destinationDirectory = file("${projectDir}/build/classes/java/main")
- kotlinOptions {
- jvmTarget = "1.8"
- }
+ compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
}
afterEvaluate {