diff options
29 files changed, 1305 insertions, 72 deletions
diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp index 5c55a435b463..c037c400948f 100644 --- a/packages/SettingsLib/tests/robotests/Android.bp +++ b/packages/SettingsLib/tests/robotests/Android.bp @@ -42,7 +42,10 @@ android_robolectric_test { name: "SettingsLibRoboTests", srcs: ["src/**/*.java"], static_libs: [ + "Settings_robolectric_meta_service_file", + "Robolectric_shadows_androidx_fragment_upstream", "SettingsLib-robo-testutils", + "androidx.fragment_fragment", "androidx.test.core", "androidx.core_core", "testng", // TODO: remove once JUnit on Android provides assertThrows @@ -53,6 +56,20 @@ android_robolectric_test { test_options: { timeout: 36000, }, + upstream: true, +} + +java_genrule { + name: "Settings_robolectric_meta_service_file", + out: ["robolectric_meta_service_file.jar"], + tools: ["soong_zip"], + cmd: "mkdir -p $(genDir)/META-INF/services/ && touch $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider &&" + + "echo -e 'org.robolectric.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " + + "echo -e 'org.robolectric.shadows.multidex.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " + + "echo -e 'org.robolectric.shadows.httpclient.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " + + //"echo -e 'com.android.settings.testutils.shadow.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " + + "echo -e 'com.android.settingslib.testutils.shadow.Shadows' >> $(genDir)/META-INF/services/org.robolectric.internal.ShadowProvider && " + + "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)/META-INF/services/", } java_library { @@ -60,9 +77,23 @@ java_library { srcs: [ "testutils/com/android/settingslib/testutils/**/*.java", ], - + javacflags: [ + "-Aorg.robolectric.annotation.processing.shadowPackage=com.android.settingslib.testutils.shadow", + "-Aorg.robolectric.annotation.processing.sdkCheckMode=ERROR", + // Uncomment the below to debug annotation processors not firing. + //"-verbose", + //"-XprintRounds", + //"-XprintProcessorInfo", + //"-Xlint", + //"-J-verbose", + ], + plugins: [ + "auto_value_plugin_1.9", + "auto_value_builder_plugin_1.9", + "Robolectric_processor_upstream", + ], libs: [ - "Robolectric_all-target", + "Robolectric_all-target_upstream", "mockito-robolectric-prebuilt", "truth-prebuilt", ], diff --git a/packages/SettingsLib/tests/robotests/config/robolectric.properties b/packages/SettingsLib/tests/robotests/config/robolectric.properties index fab7251d020b..2a9e50df62b0 100644 --- a/packages/SettingsLib/tests/robotests/config/robolectric.properties +++ b/packages/SettingsLib/tests/robotests/config/robolectric.properties @@ -1 +1,2 @@ sdk=NEWEST_SDK +instrumentedPackages=androidx.preference diff --git a/packages/SettingsLib/tests/robotests/fragment/Android.bp b/packages/SettingsLib/tests/robotests/fragment/Android.bp new file mode 100644 index 000000000000..3e67156af0c4 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/Android.bp @@ -0,0 +1,40 @@ +//############################################# +// Compile Robolectric shadows framework misapplied to androidx +//############################################# + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library { + name: "Robolectric_shadows_androidx_fragment_upstream", + srcs: [ + "src/main/java/**/*.java", + "src/main/java/**/*.kt", + ], + javacflags: [ + "-Aorg.robolectric.annotation.processing.shadowPackage=org.robolectric.shadows.androidx.fragment", + "-Aorg.robolectric.annotation.processing.sdkCheckMode=ERROR", + // Uncomment the below to debug annotation processors not firing. + //"-verbose", + //"-XprintRounds", + //"-XprintProcessorInfo", + //"-Xlint", + //"-J-verbose", + ], + libs: [ + "Robolectric_all-target_upstream", + "androidx.fragment_fragment", + ], + plugins: [ + "auto_value_plugin_1.9", + "auto_value_builder_plugin_1.9", + "Robolectric_processor_upstream", + ], + +} diff --git a/packages/SettingsLib/tests/robotests/fragment/BUILD b/packages/SettingsLib/tests/robotests/fragment/BUILD new file mode 100644 index 000000000000..393a02e8464c --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/BUILD @@ -0,0 +1,69 @@ +load("//third_party/java/android/android_sdk_linux/extras/android/compatibility/jetify:jetify.bzl", "jetify_android_library", "jetify_android_local_test") + +package( + default_applicable_licenses = ["//third_party/java_src/robolectric:license"], + default_visibility = ["//third_party/java_src/robolectric:__subpackages__"], +) + +licenses(["notice"]) + +#============================================================================== +# Test resources library +#============================================================================== +jetify_android_library( + name = "test_resources", + custom_package = "org.robolectric.shadows.androidx.fragment", + manifest = "src/test/AndroidManifest.xml", + resource_files = glob( + ["src/test/resources/**/*"], + ), +) + +#============================================================================== +# AndroidX fragment module library +#============================================================================== +jetify_android_library( + name = "androidx_fragment", + testonly = 1, + srcs = glob( + ["src/main/java/**"], + ), + custom_package = "org.robolectric.shadows.androidx.fragment", + javacopts = [ + "-Aorg.robolectric.annotation.processing.shadowPackage=org.robolectric.shadows.androidx.fragment", + ], + jetify_sources = True, + plugins = [ + "//java/com/google/thirdparty/robolectric/processor", + ], + deps = [ + "//third_party/java/androidx/core", + "//third_party/java/androidx/fragment", + "//third_party/java/androidx/lifecycle", + "//third_party/java_src/robolectric/shadowapi", + "//third_party/java_src/robolectric/shadows/framework", + ], +) + +[ + jetify_android_local_test( + name = "test_" + src.rstrip(".java"), + size = "small", + srcs = glob( + ["src/test/java/**/*.java"], + ), + jetify_sources = True, + deps = [ + ":androidx_fragment", + ":test_resources", + "//third_party/java/androidx/fragment", + "//third_party/java/androidx/loader", + "//third_party/java/mockito", + "//third_party/java/robolectric", + "//third_party/java/truth", + ], + ) + for src in glob( + ["src/test/java/**/*Test.java"], + ) +] diff --git a/packages/SettingsLib/tests/robotests/fragment/build.gradle b/packages/SettingsLib/tests/robotests/fragment/build.gradle new file mode 100644 index 000000000000..d9dcd84ded89 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/build.gradle @@ -0,0 +1,48 @@ +plugins { + id "net.ltgt.errorprone" version "0.0.13" +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + android { + sourceSets { + main { + res.srcDirs = ['src/test/resources/res'] + } + } + testOptions { + unitTests { + includeAndroidResources = true + } + } + } +} + +dependencies { + // Project dependencies + compileOnly project(":robolectric") + + // Compile dependencies + compileOnly AndroidSdk.MAX_SDK.coordinates + compileOnly "androidx.core:core:1.0.0-rc02" + compileOnly 'androidx.fragment:fragment:1.0.0-rc02' + compileOnly "androidx.lifecycle:lifecycle-viewmodel:2.0.0-rc01" + compileOnly "androidx.lifecycle:lifecycle-common:2.0.0-beta01" + + // Testing dependencies + testImplementation "com.google.truth:truth:0.44" + testImplementation "org.mockito:mockito-core:2.5.4" + testImplementation "androidx.arch.core:core-common:2.0.0-beta01" + testImplementation "androidx.arch.core:core-runtime:2.0.0-rc01" + testImplementation "androidx.collection:collection:1.0.0-rc01" + testImplementation "androidx.core:core:1.0.0-rc02" + testImplementation 'androidx.fragment:fragment:1.0.0-rc02' + testImplementation "androidx.lifecycle:lifecycle-viewmodel:2.0.0-rc01" + testImplementation "androidx.lifecycle:lifecycle-common:2.0.0-beta01" + testImplementation "androidx.lifecycle:lifecycle-runtime:2.0.0-rc01" + testImplementation "androidx.lifecycle:lifecycle-livedata-core:2.0.0-rc01" + testImplementation "androidx.loader:loader:1.0.0-rc02" +} diff --git a/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/FragmentController.java b/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/FragmentController.java new file mode 100644 index 000000000000..c688683e7f8a --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/FragmentController.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2023 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.shadows.androidx.fragment; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.LinearLayout; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; + +import org.robolectric.android.controller.ActivityController; +import org.robolectric.android.controller.ComponentController; +import org.robolectric.util.ReflectionHelpers; + +/** A Controller that can be used to drive the lifecycle of a {@link Fragment} */ +public class FragmentController<F extends Fragment> + extends ComponentController<FragmentController<F>, F> { + + private final F mFragment; + private final ActivityController<? extends FragmentActivity> mActivityController; + + private FragmentController(F fragment, Class<? extends FragmentActivity> activityClass) { + this(fragment, activityClass, null /*intent*/, null /*arguments*/); + } + + private FragmentController( + F fragment, Class<? extends FragmentActivity> activityClass, Intent intent) { + this(fragment, activityClass, intent, null /*arguments*/); + } + + private FragmentController( + F fragment, Class<? extends FragmentActivity> activityClass, Bundle arguments) { + this(fragment, activityClass, null /*intent*/, arguments); + } + + private FragmentController( + F fragment, + Class<? extends FragmentActivity> activityClass, + Intent intent, + Bundle arguments) { + super(fragment, intent); + this.mFragment = fragment; + if (arguments != null) { + this.mFragment.setArguments(arguments); + } + this.mActivityController = + ActivityController.of(ReflectionHelpers.callConstructor(activityClass), intent); + } + + /** + * Generate the {@link FragmentController} for specific fragment. + * + * @param fragment the fragment which you'd like to drive lifecycle + * @return {@link FragmentController} + */ + public static <F extends Fragment> FragmentController<F> of(F fragment) { + return new FragmentController<>(fragment, FragmentControllerActivity.class); + } + + /** + * Generate the {@link FragmentController} for specific fragment and intent. + * + * @param fragment the fragment which you'd like to drive lifecycle + * @param intent the intent which will be retained by activity + * @return {@link FragmentController} + */ + public static <F extends Fragment> FragmentController<F> of(F fragment, Intent intent) { + return new FragmentController<>(fragment, FragmentControllerActivity.class, intent); + } + + /** + * Generate the {@link FragmentController} for specific fragment and arguments. + * + * @param fragment the fragment which you'd like to drive lifecycle + * @param arguments the arguments which will be retained by fragment + * @return {@link FragmentController} + */ + public static <F extends Fragment> FragmentController<F> of(F fragment, Bundle arguments) { + return new FragmentController<>(fragment, FragmentControllerActivity.class, arguments); + } + + /** + * Generate the {@link FragmentController} for specific fragment and activity class. + * + * @param fragment the fragment which you'd like to drive lifecycle + * @param activityClass the activity which will be attached by fragment + * @return {@link FragmentController} + */ + public static <F extends Fragment> FragmentController<F> of( + F fragment, Class<? extends FragmentActivity> activityClass) { + return new FragmentController<>(fragment, activityClass); + } + + /** + * Generate the {@link FragmentController} for specific fragment, intent and arguments. + * + * @param fragment the fragment which you'd like to drive lifecycle + * @param intent the intent which will be retained by activity + * @param arguments the arguments which will be retained by fragment + * @return {@link FragmentController} + */ + public static <F extends Fragment> FragmentController<F> of( + F fragment, Intent intent, Bundle arguments) { + return new FragmentController<>(fragment, FragmentControllerActivity.class, intent, + arguments); + } + + /** + * Generate the {@link FragmentController} for specific fragment, activity class and intent. + * + * @param fragment the fragment which you'd like to drive lifecycle + * @param activityClass the activity which will be attached by fragment + * @param intent the intent which will be retained by activity + * @return {@link FragmentController} + */ + public static <F extends Fragment> FragmentController<F> of( + F fragment, Class<? extends FragmentActivity> activityClass, Intent intent) { + return new FragmentController<>(fragment, activityClass, intent); + } + + /** + * Generate the {@link FragmentController} for specific fragment, activity class and arguments. + * + * @param fragment the fragment which you'd like to drive lifecycle + * @param activityClass the activity which will be attached by fragment + * @param arguments the arguments which will be retained by fragment + * @return {@link FragmentController} + */ + public static <F extends Fragment> FragmentController<F> of( + F fragment, Class<? extends FragmentActivity> activityClass, Bundle arguments) { + return new FragmentController<>(fragment, activityClass, arguments); + } + + /** + * Generate the {@link FragmentController} for specific fragment, activity class, intent and + * arguments. + * + * @param fragment the fragment which you'd like to drive lifecycle + * @param activityClass the activity which will be attached by fragment + * @param intent the intent which will be retained by activity + * @param arguments the arguments which will be retained by fragment + * @return {@link FragmentController} + */ + public static <F extends Fragment> FragmentController<F> of( + F fragment, + Class<? extends FragmentActivity> activityClass, + Intent intent, + Bundle arguments) { + return new FragmentController<>(fragment, activityClass, intent, arguments); + } + + /** + * Sets up the given fragment by attaching it to an activity, calling its onCreate() through + * onResume() lifecycle methods, and then making it visible. Note that the fragment will be + * added + * to the view with ID 1. + */ + public static <F extends Fragment> F setupFragment(F fragment) { + return FragmentController.of(fragment).create().start().resume().visible().get(); + } + + /** + * Sets up the given fragment by attaching it to an activity, calling its onCreate() through + * onResume() lifecycle methods, and then making it visible. Note that the fragment will be + * added + * to the view with ID 1. + */ + public static <F extends Fragment> F setupFragment( + F fragment, Class<? extends FragmentActivity> fragmentActivityClass) { + return FragmentController.of(fragment, fragmentActivityClass) + .create() + .start() + .resume() + .visible() + .get(); + } + + /** + * Sets up the given fragment by attaching it to an activity created with the given bundle, + * calling its onCreate() through onResume() lifecycle methods, and then making it visible. Note + * that the fragment will be added to the view with ID 1. + */ + public static <F extends Fragment> F setupFragment( + F fragment, Class<? extends FragmentActivity> fragmentActivityClass, Bundle bundle) { + return FragmentController.of(fragment, fragmentActivityClass) + .create(bundle) + .start() + .resume() + .visible() + .get(); + } + + /** + * Sets up the given fragment by attaching it to an activity created with the given bundle and + * container id, calling its onCreate() through onResume() lifecycle methods, and then making it + * visible. + */ + public static <F extends Fragment> F setupFragment( + F fragment, + Class<? extends FragmentActivity> fragmentActivityClass, + int containerViewId, + Bundle bundle) { + return FragmentController.of(fragment, fragmentActivityClass) + .create(containerViewId, bundle) + .start() + .resume() + .visible() + .get(); + } + + /** + * Creates the activity with {@link Bundle} and adds the fragment to the view with ID {@code + * contentViewId}. + */ + public FragmentController<F> create(final int contentViewId, final Bundle bundle) { + shadowMainLooper.runPaused( + new Runnable() { + @Override + public void run() { + mActivityController + .create(bundle) + .get() + .getSupportFragmentManager() + .beginTransaction() + .add(contentViewId, mFragment) + .commit(); + } + }); + return this; + } + + /** + * Creates the activity with {@link Bundle} and adds the fragment to it. Note that the fragment + * will be added to the view with ID 1. + */ + public FragmentController<F> create(final Bundle bundle) { + return create(1, bundle); + } + + /** + * Creates the {@link Fragment} in a newly initialized state and hence will receive a null + * savedInstanceState {@link Bundle parameter} + */ + @Override + public FragmentController<F> create() { + return create(null); + } + + /** Drive lifecycle of activity to Start lifetime */ + public FragmentController<F> start() { + shadowMainLooper.runPaused( + new Runnable() { + @Override + public void run() { + mActivityController.start(); + } + }); + return this; + } + + /** Drive lifecycle of activity to Resume lifetime */ + public FragmentController<F> resume() { + shadowMainLooper.runPaused( + new Runnable() { + @Override + public void run() { + mActivityController.resume(); + } + }); + return this; + } + + /** Drive lifecycle of activity to Pause lifetime */ + public FragmentController<F> pause() { + shadowMainLooper.runPaused( + new Runnable() { + @Override + public void run() { + mActivityController.pause(); + } + }); + return this; + } + + /** Drive lifecycle of activity to Stop lifetime */ + public FragmentController<F> stop() { + shadowMainLooper.runPaused( + new Runnable() { + @Override + public void run() { + mActivityController.stop(); + } + }); + return this; + } + + /** Drive lifecycle of activity to Destroy lifetime */ + @Override + public FragmentController<F> destroy() { + shadowMainLooper.runPaused( + new Runnable() { + @Override + public void run() { + mActivityController.destroy(); + } + }); + return this; + } + + /** Let activity can be visible lifetime */ + public FragmentController<F> visible() { + shadowMainLooper.runPaused( + new Runnable() { + @Override + public void run() { + mActivityController.visible(); + } + }); + return this; + } + + private static class FragmentControllerActivity extends FragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + LinearLayout view = new LinearLayout(this); + view.setId(1); + + setContentView(view); + } + } +} diff --git a/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/package-info.java b/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/package-info.java new file mode 100644 index 000000000000..dd89441255c6 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/src/main/java/org/robolectric/shadows/androidx/fragment/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 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. + */ + +/** + * Testing infrastructure for androidx.fragment library. + * + * <p>To use this in your project, add the artifact {@code + * org.robolectric:shadows-androidx-fragment} to your project. + */ +package org.robolectric.shadows.androidx.fragment; diff --git a/packages/SettingsLib/tests/robotests/fragment/src/test/AndroidManifest.xml b/packages/SettingsLib/tests/robotests/fragment/src/test/AndroidManifest.xml new file mode 100644 index 000000000000..8493c0296c8b --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/src/test/AndroidManifest.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.robolectric.shadows.androidx.fragment"> + + <uses-sdk android:targetSdkVersion="28"/> +</manifest> diff --git a/packages/SettingsLib/tests/robotests/fragment/src/test/java/org/robolectric/shadows/androidx/fragment/FragmentControllerTest.java b/packages/SettingsLib/tests/robotests/fragment/src/test/java/org/robolectric/shadows/androidx/fragment/FragmentControllerTest.java new file mode 100644 index 000000000000..ef6305869b1f --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/src/test/java/org/robolectric/shadows/androidx/fragment/FragmentControllerTest.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2023 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.shadows.androidx.fragment; + +import static android.os.Looper.getMainLooper; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.ArrayList; +import java.util.List; + +/** Tests for {@link FragmentController} */ +@RunWith(RobolectricTestRunner.class) +public class FragmentControllerTest { + + @After + public void tearDown() { + TranscriptFragment.clearLifecycleEvents(); + } + + @Test + public void initialNotAttached() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment()); + + assertThat(controller.get().getView()).isNull(); + assertThat(controller.get().getActivity()).isNull(); + assertThat(controller.get().isAdded()).isFalse(); + } + + @Test + public void initialNotAttached_customActivity() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class); + + assertThat(controller.get().getView()).isNull(); + assertThat(controller.get().getActivity()).isNull(); + assertThat(controller.get().isAdded()).isFalse(); + } + + @Test + public void attachedAfterCreate() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment()); + + controller.create(); + shadowOf(getMainLooper()).idle(); + + assertThat(controller.get().getActivity()).isNotNull(); + assertThat(controller.get().isAdded()).isTrue(); + assertThat(controller.get().isResumed()).isFalse(); + } + + @Test + public void attachedAfterCreate_customActivity() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class); + + controller.create(); + shadowOf(getMainLooper()).idle(); + + assertThat(controller.get().getActivity()).isNotNull(); + assertThat(controller.get().getActivity()).isInstanceOf(TestActivity.class); + assertThat(controller.get().isAdded()).isTrue(); + assertThat(controller.get().isResumed()).isFalse(); + } + + @Test + public void attachedAfterCreate_customizedViewId() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), CustomizedViewIdTestActivity.class); + + controller.create(R.id.custom_activity_view, null).start(); + + assertThat(controller.get().getView()).isNotNull(); + assertThat(controller.get().getActivity()).isNotNull(); + assertThat(controller.get().isAdded()).isTrue(); + assertThat(controller.get().isResumed()).isFalse(); + assertThat((TextView) controller.get().getView().findViewById(R.id.tacos)).isNotNull(); + } + + @Test + public void hasViewAfterStart() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment()); + + controller.create().start(); + + assertThat(controller.get().getView()).isNotNull(); + } + + @Test + public void isResumed() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class); + + controller.create().start().resume(); + + assertThat(controller.get().getView()).isNotNull(); + assertThat(controller.get().getActivity()).isNotNull(); + assertThat(controller.get().isAdded()).isTrue(); + assertThat(controller.get().isResumed()).isTrue(); + assertThat((TextView) controller.get().getView().findViewById(R.id.tacos)).isNotNull(); + } + + @Test + public void isPaused() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class); + + controller.create().start().resume().pause(); + + assertThat(controller.get().getView()).isNotNull(); + assertThat(controller.get().getActivity()).isNotNull(); + assertThat(controller.get().isAdded()).isTrue(); + assertThat(controller.get().isResumed()).isFalse(); + assertThat(controller.get().getLifecycleEvents()) + .containsExactly("onCreate", "onStart", "onResume", "onPause") + .inOrder(); + } + + @Test + public void isStopped() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class); + + controller.create().start().resume().pause().stop(); + + assertThat(controller.get().getView()).isNotNull(); + assertThat(controller.get().getActivity()).isNotNull(); + assertThat(controller.get().isAdded()).isTrue(); + assertThat(controller.get().isResumed()).isFalse(); + assertThat(controller.get().getLifecycleEvents()) + .containsExactly("onCreate", "onStart", "onResume", "onPause", "onStop") + .inOrder(); + } + + @Test + public void withIntent() { + final Intent intent = generateTestIntent(); + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class, intent); + + controller.create(); + shadowOf(getMainLooper()).idle(); + final Intent intentInFragment = controller.get().getActivity().getIntent(); + + assertThat(intentInFragment.getAction()).isEqualTo("test_action"); + assertThat(intentInFragment.getExtras().getString("test_key")).isEqualTo("test_value"); + } + + @Test + public void withArguments() { + final Bundle bundle = generateTestBundle(); + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class, bundle); + + controller.create(); + final Bundle args = controller.get().getArguments(); + + assertThat(args.getString("test_key")).isEqualTo("test_value"); + } + + @Test + public void withIntentAndArguments() { + final Bundle bundle = generateTestBundle(); + final Intent intent = generateTestIntent(); + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class, intent, bundle); + + controller.create(); + shadowOf(getMainLooper()).idle(); + final Intent intentInFragment = controller.get().getActivity().getIntent(); + final Bundle args = controller.get().getArguments(); + + assertThat(intentInFragment.getAction()).isEqualTo("test_action"); + assertThat(intentInFragment.getExtras().getString("test_key")).isEqualTo("test_value"); + assertThat(args.getString("test_key")).isEqualTo("test_value"); + } + + @Test + public void visible() { + final FragmentController<TranscriptFragment> controller = + FragmentController.of(new TranscriptFragment(), TestActivity.class); + + controller.create().start().resume(); + + assertThat(controller.get().isVisible()).isFalse(); + + controller.visible(); + + assertThat(controller.get().isVisible()).isTrue(); + } + + @Test + public void setupFragmentWithFragment_fragmentHasCorrectLifecycle() { + TranscriptFragment fragment = FragmentController.setupFragment(new TranscriptFragment()); + + assertThat(fragment.getLifecycleEvents()) + .containsExactly("onCreate", "onStart", "onResume") + .inOrder(); + assertThat(fragment.isVisible()).isTrue(); + } + + @Test + public void setupFragmentWithFragmentAndActivity_fragmentHasCorrectLifecycle() { + TranscriptFragment fragment = + FragmentController.setupFragment(new TranscriptFragment(), TestActivity.class); + + assertThat(fragment.getLifecycleEvents()) + .containsExactly("onCreate", "onStart", "onResume") + .inOrder(); + assertThat(fragment.isVisible()).isTrue(); + } + + @Test + public void setupFragmentWithFragmentAndActivityAndBundle_HasCorrectLifecycle() { + Bundle testBundle = generateTestBundle(); + TranscriptFragment fragment = + FragmentController.setupFragment(new TranscriptFragment(), TestActivity.class, + testBundle); + + assertThat(fragment.getLifecycleEvents()) + .containsExactly("onCreate", "onStart", "onResume") + .inOrder(); + assertThat(fragment.isVisible()).isTrue(); + } + + @Test + public void + setupFragmentWithFragment_Activity_ContainViewIdAndBundle_HasCorrectLifecycle() { + Bundle testBundle = generateTestBundle(); + TranscriptFragment fragment = + FragmentController.setupFragment( + new TranscriptFragment(), + CustomizedViewIdTestActivity.class, + R.id.custom_activity_view, + testBundle); + + assertThat(fragment.getLifecycleEvents()) + .containsExactly("onCreate", "onStart", "onResume") + .inOrder(); + assertThat(fragment.isVisible()).isTrue(); + } + + private Intent generateTestIntent() { + final Intent testIntent = new Intent("test_action").putExtra("test_key", "test_value"); + return testIntent; + } + + private Bundle generateTestBundle() { + final Bundle testBundle = new Bundle(); + testBundle.putString("test_key", "test_value"); + + return testBundle; + } + + /** A Fragment which can record lifecycle status for test. */ + public static class TranscriptFragment extends Fragment { + + public static final List<String> sLifecycleEvents = new ArrayList<>(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sLifecycleEvents.add("onCreate"); + } + + @Override + public void onStart() { + super.onStart(); + sLifecycleEvents.add("onStart"); + } + + @Override + public void onResume() { + super.onResume(); + sLifecycleEvents.add("onResume"); + } + + @Override + public void onPause() { + super.onPause(); + sLifecycleEvents.add("onPause"); + } + + @Override + public void onStop() { + super.onStop(); + sLifecycleEvents.add("onStop"); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_contents, container, false); + } + + public List<String> getLifecycleEvents() { + return sLifecycleEvents; + } + + public static void clearLifecycleEvents() { + sLifecycleEvents.clear(); + } + } + + /** A Activity which set a default view for test. */ + public static class TestActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + LinearLayout view = new LinearLayout(this); + view.setId(1); + + setContentView(view); + } + } + + /** A Activity which has a custom view for test. */ + public static class CustomizedViewIdTestActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.custom_activity_view); + } + } +} diff --git a/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/custom_activity_view.xml b/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/custom_activity_view.xml new file mode 100644 index 000000000000..c074f30964db --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/custom_activity_view.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/custom_activity_view" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical"> + +</LinearLayout> diff --git a/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/fragment_contents.xml b/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/fragment_contents.xml new file mode 100644 index 000000000000..425b2bb6a0d9 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/fragment/src/test/resources/res/layout/fragment_contents.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/tacos" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="TACOS"/> + + <TextView + android:id="@+id/burritos" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="BURRITOS"/> + +</LinearLayout> diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java index 4a913c87bddf..bb72375499c1 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java @@ -25,7 +25,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.app.ActivityManager; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -58,12 +57,10 @@ import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowSettings; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; @RunWith(RobolectricTestRunner.class) -@Config(shadows = {UtilsTest.ShadowSecure.class, UtilsTest.ShadowLocationManager.class}) +@Config(shadows = {UtilsTest.ShadowLocationManager.class}) public class UtilsTest { private static final double[] TEST_PERCENTAGES = {0, 0.4, 0.5, 0.6, 49, 49.3, 49.8, 50, 100}; private static final String TAG = "UtilsTest"; @@ -94,7 +91,7 @@ public class UtilsTest { mContext = spy(RuntimeEnvironment.application); when(mContext.getSystemService(Context.LOCATION_SERVICE)).thenReturn(mLocationManager); when(mContext.getSystemService(UsbManager.class)).thenReturn(mUsbManager); - ShadowSecure.reset(); + ShadowSettings.ShadowSecure.reset(); mAudioManager = mContext.getSystemService(AudioManager.class); } @@ -111,15 +108,16 @@ public class UtilsTest { Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS); assertThat(Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.LOCATION_CHANGER, Settings.Secure.LOCATION_CHANGER_UNKNOWN)) - .isEqualTo(Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS); + Settings.Secure.LOCATION_CHANGER, + Settings.Secure.LOCATION_CHANGER_UNKNOWN)).isEqualTo( + Settings.Secure.LOCATION_CHANGER_QUICK_SETTINGS); } @Test public void testFormatPercentage_RoundTrue_RoundUpIfPossible() { - final String[] expectedPercentages = {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_1, - PERCENTAGE_1, PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_50, PERCENTAGE_50, - PERCENTAGE_100}; + final String[] expectedPercentages = + {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_1, PERCENTAGE_1, PERCENTAGE_49, + PERCENTAGE_49, PERCENTAGE_50, PERCENTAGE_50, PERCENTAGE_100}; for (int i = 0, size = TEST_PERCENTAGES.length; i < size; i++) { final String percentage = Utils.formatPercentage(TEST_PERCENTAGES[i], true); @@ -129,9 +127,9 @@ public class UtilsTest { @Test public void testFormatPercentage_RoundFalse_NoRound() { - final String[] expectedPercentages = {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_0, - PERCENTAGE_0, PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_50, - PERCENTAGE_100}; + final String[] expectedPercentages = + {PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_0, PERCENTAGE_49, + PERCENTAGE_49, PERCENTAGE_49, PERCENTAGE_50, PERCENTAGE_100}; for (int i = 0, size = TEST_PERCENTAGES.length; i < size; i++) { final String percentage = Utils.formatPercentage(TEST_PERCENTAGES[i], false); @@ -143,12 +141,7 @@ public class UtilsTest { public void testGetDefaultStorageManagerDaysToRetain_storageManagerDaysToRetainUsesResources() { Resources resources = mock(Resources.class); when(resources.getInteger( - eq( - com.android - .internal - .R - .integer - .config_storageManagerDaystoRetainDefault))) + eq(com.android.internal.R.integer.config_storageManagerDaystoRetainDefault))) .thenReturn(60); assertThat(Utils.getDefaultStorageManagerDaysToRetain(resources)).isEqualTo(60); } @@ -163,31 +156,6 @@ public class UtilsTest { return intent -> TextUtils.equals(expected, intent.getAction()); } - @Implements(value = Settings.Secure.class) - public static class ShadowSecure extends ShadowSettings.ShadowSecure { - private static Map<String, Integer> map = new HashMap<>(); - - @Implementation - public static boolean putIntForUser(ContentResolver cr, String name, int value, - int userHandle) { - map.put(name, value); - return true; - } - - @Implementation - public static int getIntForUser(ContentResolver cr, String name, int def, int userHandle) { - if (map.containsKey(name)) { - return map.get(name); - } else { - return def; - } - } - - public static void reset() { - map.clear(); - } - } - @Implements(value = LocationManager.class) public static class ShadowLocationManager { @@ -337,9 +305,8 @@ public class UtilsTest { @Test public void getBatteryStatus_statusIsFull_returnFullString() { - final Intent intent = new Intent() - .putExtra(BatteryManager.EXTRA_LEVEL, 100) - .putExtra(BatteryManager.EXTRA_SCALE, 100); + final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_LEVEL, 100).putExtra( + BatteryManager.EXTRA_SCALE, 100); final Resources resources = mContext.getResources(); assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false)).isEqualTo( @@ -348,9 +315,8 @@ public class UtilsTest { @Test public void getBatteryStatus_statusIsFullAndUseCompactStatus_returnFullyChargedString() { - final Intent intent = new Intent() - .putExtra(BatteryManager.EXTRA_LEVEL, 100) - .putExtra(BatteryManager.EXTRA_SCALE, 100); + final Intent intent = new Intent().putExtra(BatteryManager.EXTRA_LEVEL, 100).putExtra( + BatteryManager.EXTRA_SCALE, 100); final Resources resources = mContext.getResources(); assertThat(Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ true)).isEqualTo( @@ -516,7 +482,6 @@ public class UtilsTest { when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus); when(mUsbPort.supportsComplianceWarnings()).thenReturn(true); when(mUsbPortStatus.isConnected()).thenReturn(true); - when(mUsbPortStatus.getComplianceWarnings()) - .thenReturn(new int[]{complianceWarningType}); + when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{complianceWarningType}); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/accessibility/AccessibilityUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/accessibility/AccessibilityUtilsTest.java index 44fdaec49f73..3de84464af2e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/accessibility/AccessibilityUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/accessibility/AccessibilityUtilsTest.java @@ -23,13 +23,17 @@ import android.content.Context; import android.os.UserHandle; import android.provider.Settings; +import com.android.settingslib.testutils.shadow.ShadowSecure; + 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(shadows = {ShadowSecure.class}) public class AccessibilityUtilsTest { private Context mContext; @@ -46,7 +50,7 @@ public class AccessibilityUtilsTest { @Test public void getEnabledServicesFromSettings_badFormat_emptyResult() { - Settings.Secure.putStringForUser( + ShadowSecure.putStringForUser( mContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, ":", UserHandle.myUserId()); @@ -57,7 +61,7 @@ public class AccessibilityUtilsTest { @Test public void getEnabledServicesFromSettings_1Service_1result() { final ComponentName cn = new ComponentName("pkg", "serv"); - Settings.Secure.putStringForUser( + ShadowSecure.putStringForUser( mContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, cn.flattenToString() + ":", UserHandle.myUserId()); @@ -70,7 +74,7 @@ public class AccessibilityUtilsTest { public void getEnabledServicesFromSettings_2Services_2results() { final ComponentName cn1 = new ComponentName("pkg", "serv"); final ComponentName cn2 = new ComponentName("pkg", "serv2"); - Settings.Secure.putStringForUser( + ShadowSecure.putStringForUser( mContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, cn1.flattenToString() + ":" + cn2.flattenToString(), UserHandle.myUserId()); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java index cb62a735434d..f9505ddb7e2f 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java @@ -37,6 +37,8 @@ import android.os.UserHandle; import android.os.UserManager; import android.util.LongSparseArray; +import com.android.settingslib.testutils.shadow.ShadowPermissionChecker; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,7 +47,6 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowPermissionChecker; import java.time.Clock; import java.util.ArrayList; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java index dd8d54a62ff4..a2e8c5956100 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java @@ -38,6 +38,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.LooperMode; import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; @@ -167,6 +168,7 @@ public class MetricsFeatureProviderTest { } @Test + @LooperMode(LooperMode.Mode.PAUSED) public void getAttribution_notSet_shouldReturnUnknown() { final Activity activity = Robolectric.setupActivity(Activity.class); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java index d67d44b9035d..25833b3ddbba 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/SettingsJankMonitorTest.java @@ -36,6 +36,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.CujType; +import com.android.settingslib.testutils.OverpoweredReflectionHelper; import com.android.settingslib.testutils.shadow.ShadowInteractionJankMonitor; import org.junit.Before; @@ -51,7 +52,6 @@ import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; -import org.robolectric.util.ReflectionHelpers; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -83,8 +83,10 @@ public class SettingsJankMonitorTest { public void setUp() { ShadowInteractionJankMonitor.reset(); when(ShadowInteractionJankMonitor.MOCK_INSTANCE.begin(any())).thenReturn(true); - ReflectionHelpers.setStaticField(SettingsJankMonitor.class, "scheduledExecutorService", - mScheduledExecutorService); + OverpoweredReflectionHelper + .setStaticField(SettingsJankMonitor.class, + "scheduledExecutorService", + mScheduledExecutorService); } @Test diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/HideNonSystemOverlayMixinTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/HideNonSystemOverlayMixinTest.java index cf702b531a3c..471dac05e6b4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/HideNonSystemOverlayMixinTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/lifecycle/HideNonSystemOverlayMixinTest.java @@ -37,8 +37,10 @@ import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.android.controller.ActivityController; +import org.robolectric.annotation.LooperMode; @RunWith(RobolectricTestRunner.class) +@LooperMode(LooperMode.Mode.PAUSED) public class HideNonSystemOverlayMixinTest { private ActivityController<TestActivity> mActivityController; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/development/DevelopmentSettingsEnablerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/development/DevelopmentSettingsEnablerTest.java index 3475ff7d96f8..b009abd87b1c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/development/DevelopmentSettingsEnablerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/development/DevelopmentSettingsEnablerTest.java @@ -22,13 +22,14 @@ import android.content.Context; import android.os.UserManager; import android.provider.Settings; +import com.android.settingslib.testutils.shadow.ShadowUserManager; + import org.junit.Before; 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.ShadowUserManager; @RunWith(RobolectricTestRunner.class) public class DevelopmentSettingsEnablerTest { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java index 8e33ca338eb1..0cabab241be4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.LooperMode; import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; @@ -269,6 +270,7 @@ public class LicenseHtmlGeneratorFromXmlTest { } @Test + @LooperMode(LooperMode.Mode.PAUSED) public void testGenerateHtmlWithCustomHeading() throws Exception { List<File> xmlFiles = new ArrayList<>(); Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap = new HashMap<>(); @@ -292,6 +294,7 @@ public class LicenseHtmlGeneratorFromXmlTest { } @Test + @LooperMode(LooperMode.Mode.PAUSED) public void testGenerateNewHtmlWithCustomHeading() throws Exception { List<File> xmlFiles = new ArrayList<>(); Map<String, Map<String, Set<String>>> fileNameToLibraryToContentIdMap = new HashMap<>(); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/package-info.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/package-info.java new file mode 100644 index 000000000000..9e9725fc3710 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 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. + */ + +@LooperMode(LooperMode.Mode.LEGACY) +package com.android.settingslib; + +import org.robolectric.annotation.LooperMode; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AnimatedImageViewTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AnimatedImageViewTest.java index d41d5112e6b2..faec02f7a0d4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AnimatedImageViewTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AnimatedImageViewTest.java @@ -27,6 +27,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.LooperMode; @RunWith(RobolectricTestRunner.class) public class AnimatedImageViewTest { @@ -40,6 +41,7 @@ public class AnimatedImageViewTest { } @Test + @LooperMode(LooperMode.Mode.PAUSED) public void testAnimation_ViewVisible_AnimationRunning() { mAnimatedImageView.setVisibility(View.VISIBLE); mAnimatedImageView.setAnimating(true); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java index 0a48f19a3021..0d889139e8b4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java @@ -41,6 +41,8 @@ import androidx.annotation.ColorRes; import androidx.preference.PreferenceViewHolder; import androidx.preference.R; +import com.android.settingslib.testutils.OverpoweredReflectionHelper; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -502,14 +504,18 @@ public class BannerMessagePreferenceTest { private void assumeAndroidR() { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 30); ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "R"); - ReflectionHelpers.setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", false); + OverpoweredReflectionHelper + .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", false); // Reset view holder to use correct layout. } + + private void assumeAndroidS() { ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 31); ReflectionHelpers.setStaticField(Build.VERSION.class, "CODENAME", "S"); - ReflectionHelpers.setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", true); + OverpoweredReflectionHelper + .setStaticField(BannerMessagePreference.class, "IS_AT_LEAST_S", true); // Re-inflate view to update layout. setUpViewHolder(); } diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/OverpoweredReflectionHelper.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/OverpoweredReflectionHelper.java new file mode 100644 index 000000000000..4fcc5a1f7000 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/OverpoweredReflectionHelper.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 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 com.android.settingslib.testutils; + +import org.robolectric.util.ReflectionHelpers; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +public class OverpoweredReflectionHelper extends ReflectionHelpers { + + /** + * Robolectric upstream does not rely on or encourage this behaviour. + * + * @param field + */ + private static void makeFieldVeryAccessible(Field field) { + field.setAccessible(true); + // remove 'final' modifier if present + if ((field.getModifiers() & Modifier.FINAL) == Modifier.FINAL) { + Field modifiersField = getModifiersField(); + modifiersField.setAccessible(true); + try { + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + } catch (IllegalAccessException e) { + + throw new AssertionError(e); + } + } + } + + private static Field getModifiersField() { + try { + return Field.class.getDeclaredField("modifiers"); + } catch (NoSuchFieldException e) { + try { + Method getFieldsMethod = + Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class); + getFieldsMethod.setAccessible(true); + Field[] fields = (Field[]) getFieldsMethod.invoke(Field.class, false); + for (Field modifiersField : fields) { + if ("modifiers".equals(modifiersField.getName())) { + return modifiersField; + } + } + } catch (ReflectiveOperationException innerE) { + throw new AssertionError(innerE); + } + } + throw new AssertionError(); + } + + /** + * Reflectively set the value of a static field. + * + * @param field Field object. + * @param fieldNewValue The new value. + */ + public static void setStaticField(Field field, Object fieldNewValue) { + try { + makeFieldVeryAccessible(field); + field.setAccessible(true); + field.set(null, fieldNewValue); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Reflectively set the value of a static field. + * + * @param clazz Target class. + * @param fieldName The field name. + * @param fieldNewValue The new value. + */ + public static void setStaticField(Class<?> clazz, String fieldName, Object fieldNewValue) { + try { + setStaticField(clazz.getDeclaredField(fieldName), fieldNewValue); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowActivityManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowActivityManager.java index 924eb047c340..0b9ba8d044ce 100644 --- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowActivityManager.java +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowActivityManager.java @@ -16,23 +16,27 @@ package com.android.settingslib.testutils.shadow; +import static android.os.Build.VERSION_CODES.O; + import android.app.ActivityManager; +import android.app.IActivityManager; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.annotation.Resetter; import org.robolectric.shadow.api.Shadow; +import org.robolectric.util.ReflectionHelpers; @Implements(ActivityManager.class) public class ShadowActivityManager { private static int sCurrentUserId = 0; - private int mUserSwitchedTo = -1; + private static int sUserSwitchedTo = -1; @Resetter - public void reset() { + public static void reset() { sCurrentUserId = 0; - mUserSwitchedTo = 0; + sUserSwitchedTo = 0; } @Implementation @@ -42,16 +46,21 @@ public class ShadowActivityManager { @Implementation protected boolean switchUser(int userId) { - mUserSwitchedTo = userId; + sUserSwitchedTo = userId; return true; } + @Implementation(minSdk = O) + protected static IActivityManager getService() { + return ReflectionHelpers.createNullProxy(IActivityManager.class); + } + public boolean getSwitchUserCalled() { - return mUserSwitchedTo != -1; + return sUserSwitchedTo != -1; } public int getUserSwitchedTo() { - return mUserSwitchedTo; + return sUserSwitchedTo; } public static void setCurrentUser(int userId) { diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowDefaultDialerManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowDefaultDialerManager.java index 2c0792fd57bd..bbfdb7f91c4b 100644 --- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowDefaultDialerManager.java +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowDefaultDialerManager.java @@ -29,7 +29,7 @@ public class ShadowDefaultDialerManager { private static String sDefaultDialer; @Resetter - public void reset() { + public static void reset() { sDefaultDialer = null; } diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java new file mode 100644 index 000000000000..fae3aeafd5fb --- /dev/null +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowPermissionChecker.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 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 com.android.settingslib.testutils.shadow; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.AttributionSource; +import android.content.Context; +import android.content.PermissionChecker; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +import java.util.HashMap; +import java.util.Map; +/** Shadow class of {@link PermissionChecker}. */ +@Implements(PermissionChecker.class) +public class ShadowPermissionChecker { + private static final Map<String, Map<String, Integer>> RESULTS = new HashMap<>(); + /** Set the result of permission check for a specific permission. */ + public static void setResult(String packageName, String permission, int result) { + if (!RESULTS.containsKey(packageName)) { + RESULTS.put(packageName, new HashMap<>()); + } + RESULTS.get(packageName).put(permission, result); + } + /** Check the permission of calling package. */ + @Implementation + public static int checkCallingPermissionForDataDelivery( + Context context, + String permission, + String packageName, + String attributionTag, + String message) { + return RESULTS.containsKey(packageName) && RESULTS.get(packageName).containsKey(permission) + ? RESULTS.get(packageName).get(permission) + : PermissionChecker.checkCallingPermissionForDataDelivery( + context, permission, packageName, attributionTag, message); + } + /** Check general permission. */ + @Implementation + public static int checkPermissionForDataDelivery( + Context context, + String permission, + int pid, + int uid, + String packageName, + String attributionTag, + String message) { + return RESULTS.containsKey(packageName) && RESULTS.get(packageName).containsKey(permission) + ? RESULTS.get(packageName).get(permission) + : PermissionChecker.checkPermissionForDataDelivery( + context, permission, pid, uid, packageName, attributionTag, message); + } + /** Check general permission. */ + @Implementation + public static int checkPermissionForPreflight(@NonNull Context context, + @NonNull String permission, int pid, int uid, @Nullable String packageName) { + return checkPermissionForPreflight(context, permission, new AttributionSource( + uid, packageName, null /*attributionTag*/)); + } + /** Check general permission. */ + @Implementation + public static int checkPermissionForPreflight(@NonNull Context context, + @NonNull String permission, @NonNull AttributionSource attributionSource) { + final String packageName = attributionSource.getPackageName(); + return RESULTS.containsKey(packageName) && RESULTS.get(packageName).containsKey(permission) + ? RESULTS.get(packageName).get(permission) + : PermissionChecker.checkPermissionForPreflight( + context, permission, attributionSource); + } +} diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSecure.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSecure.java new file mode 100644 index 000000000000..70ebc6791538 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSecure.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 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 com.android.settingslib.testutils.shadow; + +import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; + +import android.content.ContentResolver; +import android.provider.Settings; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.shadows.ShadowSettings; + +@Implements(value = Settings.Secure.class) +public class ShadowSecure extends ShadowSettings.ShadowSecure { + @Implementation(minSdk = JELLY_BEAN_MR1) + public static boolean putStringForUser(ContentResolver cr, String name, String value, + int userHandle) { + return putString(cr, name, value); + } +} diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSmsApplication.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSmsApplication.java index 381d072b6fe1..5ac0a87c1ae9 100644 --- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSmsApplication.java +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowSmsApplication.java @@ -31,7 +31,7 @@ public class ShadowSmsApplication { private static ComponentName sDefaultSmsApplication; @Resetter - public void reset() { + public static void reset() { sDefaultSmsApplication = null; } diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowUserManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowUserManager.java index ca1eefcad7de..60d7721242b8 100644 --- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowUserManager.java +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowUserManager.java @@ -16,20 +16,28 @@ package com.android.settingslib.testutils.shadow; +import static android.os.Build.VERSION_CODES.N_MR1; + import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.UserInfo; +import android.content.pm.UserProperties; +import android.os.UserHandle; import android.os.UserManager; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.shadows.ShadowBuild; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Implements(value = UserManager.class) public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager { private List<UserInfo> mUserInfos = addProfile(0, "Owner"); + private final Map<Integer, UserProperties> mUserPropertiesMap = new HashMap<>(); @Implementation protected static UserManager get(Context context) { @@ -62,4 +70,37 @@ public class ShadowUserManager extends org.robolectric.shadows.ShadowUserManager protected List<UserInfo> getProfiles(@UserIdInt int userHandle) { return getProfiles(); } + + /** + * @return {@code false} by default, or the value specified via {@link #setIsAdminUser(boolean)} + */ + @Implementation(minSdk = N_MR1) + public boolean isAdminUser() { + return getUserInfo(UserHandle.myUserId()).isAdmin(); + } + + /** + * Sets that the current user is an admin user; controls the return value of + * {@link UserManager#isAdminUser}. + */ + public void setIsAdminUser(boolean isAdminUser) { + UserInfo userInfo = getUserInfo(UserHandle.myUserId()); + if (isAdminUser) { + userInfo.flags |= UserInfo.FLAG_ADMIN; + } else { + userInfo.flags &= ~UserInfo.FLAG_ADMIN; + } + } + + public void setupUserProperty(int userId, int showInSettings) { + UserProperties userProperties = new UserProperties(new UserProperties.Builder() + .setShowInSettings(showInSettings).build()); + mUserPropertiesMap.putIfAbsent(userId, userProperties); + } + + @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE) + protected UserProperties getUserProperties(UserHandle user) { + return mUserPropertiesMap.getOrDefault(user.getIdentifier(), + new UserProperties(new UserProperties.Builder().build())); + } } |