diff options
author | Louis Pullen-Freilich <lpf@google.com> | 2020-07-01 16:00:06 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-07-01 16:00:06 +0000 |
commit | 6bbc77c73ffc8f7d5c3d4a6052b7858d16e05342 (patch) | |
tree | 06d2c160e6b9b97c04d4abf14d7296edf3ef8f49 | |
parent | 1e0ffa4217fe5325aa64df3317bc1b977a52702e (diff) | |
parent | cec0e523cb6f7823bcfdf1053df302dbfaf5a597 (diff) | |
download | support-6bbc77c73ffc8f7d5c3d4a6052b7858d16e05342.tar.gz |
Merge "Fixes onPreferenceStartScreen being called twice" into androidx-master-dev
3 files changed, 434 insertions, 3 deletions
diff --git a/preference/preference/src/androidTest/AndroidManifest.xml b/preference/preference/src/androidTest/AndroidManifest.xml index 7ce0a59e634..be3101fec9f 100644 --- a/preference/preference/src/androidTest/AndroidManifest.xml +++ b/preference/preference/src/androidTest/AndroidManifest.xml @@ -17,7 +17,14 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="androidx.preference.tests"> <application> - <activity android:name="androidx.preference.tests.helpers.PreferenceTestHelperActivity" - android:theme="@style/PreferenceTestTheme"/> + <activity + android:name="androidx.preference.tests.helpers.PreferenceTestHelperActivity" + android:theme="@style/PreferenceTestTheme"/> + <activity + android:name="androidx.preference.tests.NoInterfaceTestActivity" + android:theme="@style/PreferenceTestTheme"/> + <activity + android:name="androidx.preference.tests.WithInterfaceTestActivity" + android:theme="@style/PreferenceTestTheme"/> </application> </manifest> diff --git a/preference/preference/src/androidTest/java/androidx/preference/tests/PreferenceFragmentCompatInterfaceTest.kt b/preference/preference/src/androidTest/java/androidx/preference/tests/PreferenceFragmentCompatInterfaceTest.kt new file mode 100644 index 00000000000..0a622efbb5e --- /dev/null +++ b/preference/preference/src/androidTest/java/androidx/preference/tests/PreferenceFragmentCompatInterfaceTest.kt @@ -0,0 +1,424 @@ +/* + * Copyright 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. + */ + +package androidx.preference.tests + +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.EditTextPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceScreen +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test for [PreferenceFragmentCompat] interfaces that can be implemented via both [Context] and + * [android.app.Activity], to ensure that they are called, and only once. + * + * This test doesn't test the paths including [PreferenceFragmentCompat.getCallbackFragment], as + * this API is @RestrictTo and we don't expect developers to be using it. + */ +@RunWith(AndroidJUnit4::class) +@LargeTest +class PreferenceFragmentCompatInterfaceTest { + + @Suppress("DEPRECATION") + @get:Rule + val noInterfaceActivityRule = androidx.test.rule.ActivityTestRule( + NoInterfaceTestActivity::class.java, + false, + false + ) + + @Suppress("DEPRECATION") + @get:Rule + val withInterfaceActivityRule = androidx.test.rule.ActivityTestRule( + WithInterfaceTestActivity::class.java, + false, + false + ) + + @Test + fun onPreferenceStartFragmentTest_contextCallback() { + verifyCallsWithContext { + Preference(context).apply { + fragment = "dummy.fragment" + } + } + } + + @Test + fun onPreferenceStartFragmentTest_activityCallback() { + verifyCallsWithActivity { + Preference(context).apply { + fragment = "dummy.fragment" + } + } + } + + @Test + fun onPreferenceStartFragmentTest_contextAndActivityCallback_contextHandlesCall() { + verifyCallsWithContextAndActivity(contextHandlesCall = true) { + Preference(context).apply { + fragment = "dummy.fragment" + } + } + } + + @Test + fun onPreferenceStartFragmentTest_contextAndActivityCallback_contextDoesNotHandleCall() { + verifyCallsWithContextAndActivity(contextHandlesCall = false) { + Preference(context).apply { + fragment = "dummy.fragment" + } + } + } + + @Test + fun onPreferenceStartScreenTest_contextCallback() { + verifyCallsWithContext { + preferenceManager.createPreferenceScreen(context).apply { + // Add a preference as PreferenceScreen will only dispatch + // onPreferenceStartScreen if its count != 0 + addPreference(Preference(context)) + } + } + } + + @Test + fun onPreferenceStartScreenTest_activityCallback() { + verifyCallsWithActivity { + preferenceManager.createPreferenceScreen(context).apply { + // Add a preference as PreferenceScreen will only dispatch + // onPreferenceStartScreen if its count != 0 + addPreference(Preference(context)) + } + } + } + + @Test + fun onPreferenceStartScreenTest_contextAndActivityCallback_contextHandlesCall() { + verifyCallsWithContextAndActivity(contextHandlesCall = true) { + preferenceManager.createPreferenceScreen(context).apply { + // Add a preference as PreferenceScreen will only dispatch + // onPreferenceStartScreen if its count != 0 + addPreference(Preference(context)) + } + } + } + + @Test + fun onPreferenceStartScreenTest_contextAndActivityCallback_contextDoesNotHandleCall() { + verifyCallsWithContextAndActivity(contextHandlesCall = false) { + preferenceManager.createPreferenceScreen(context).apply { + // Add a preference as PreferenceScreen will only dispatch + // onPreferenceStartScreen if its count != 0 + addPreference(Preference(context)) + } + } + } + + @Test + fun onPreferenceDisplayDialogTest_contextCallback() { + verifyCallsWithContext { + EditTextPreference(context) + } + } + + @Test + fun onPreferenceDisplayDialogTest_activityCallback() { + verifyCallsWithActivity { + EditTextPreference(context) + } + } + + @Test + fun onPreferenceDisplayDialogTest_contextAndActivityCallback_contextHandlesCall() { + verifyCallsWithContextAndActivity(contextHandlesCall = true) { + EditTextPreference(context) + } + } + + @Test + fun onPreferenceDisplayDialogTest_contextAndActivityCallback_contextDoesNotHandleCall() { + verifyCallsWithContextAndActivity(contextHandlesCall = false) { + EditTextPreference(context) + } + } + + private fun verifyCallsWithContext( + testPreferenceFactory: PreferenceFragmentCompat.() -> Preference + ) { + var count = 0 + val incrementCount: () -> Boolean = { + count++ + true + } + + noInterfaceActivityRule.launchActivity(Intent()) + + noInterfaceActivityRule.run { + runOnUiThread { + activity.displayPreferenceFragment( + TestFragment( + testPreferenceFactory, + contextCallback = incrementCount + ) + ) + } + } + + TestFragment.assertPreferenceIsDisplayed() + + noInterfaceActivityRule.runOnUiThread { + assertEquals(0, count) + } + + TestFragment.clickOnPreference() + + noInterfaceActivityRule.runOnUiThread { + assertEquals(1, count) + } + } + + private fun verifyCallsWithActivity( + testPreferenceFactory: PreferenceFragmentCompat.() -> Preference + ) { + var count = 0 + val incrementCount: () -> Boolean = { + count++ + true + } + + withInterfaceActivityRule.launchActivity(Intent()) + + withInterfaceActivityRule.run { + runOnUiThread { + activity.setTestCallback(incrementCount) + activity.displayPreferenceFragment( + TestFragment( + testPreferenceFactory, + contextCallback = null + ) + ) + } + } + + TestFragment.assertPreferenceIsDisplayed() + + withInterfaceActivityRule.runOnUiThread { + assertEquals(0, count) + } + + TestFragment.clickOnPreference() + + withInterfaceActivityRule.runOnUiThread { + assertEquals(1, count) + } + } + + /** + * @param contextHandlesCall whether the context implementation 'handles' the interface call, + * by returning true. If it does, then the activity implementation should not be called. + */ + private fun verifyCallsWithContextAndActivity( + contextHandlesCall: Boolean, + testPreferenceFactory: PreferenceFragmentCompat.() -> Preference + ) { + var contextCount = 0 + val incrementContextCount: () -> Boolean = { + contextCount++ + contextHandlesCall + } + + var activityCount = 0 + val incrementActivityCount: () -> Boolean = { + activityCount++ + true + } + + withInterfaceActivityRule.launchActivity(Intent()) + + withInterfaceActivityRule.run { + runOnUiThread { + activity.setTestCallback(incrementActivityCount) + activity.displayPreferenceFragment( + TestFragment( + testPreferenceFactory, + contextCallback = incrementContextCount + ) + ) + } + } + + TestFragment.assertPreferenceIsDisplayed() + + withInterfaceActivityRule.runOnUiThread { + assertEquals(0, contextCount) + assertEquals(0, activityCount) + } + + TestFragment.clickOnPreference() + + withInterfaceActivityRule.runOnUiThread { + // Context should be checked before activity, so this will always be called + assertEquals(1, contextCount) + + val expectedActivityCount = if (contextHandlesCall) { + // If context returns true, then we should never call the activity implementation + 0 + } else { + // If context returns false, it has not handled the call, so we should call the + // activity implementation + 1 + } + assertEquals(expectedActivityCount, activityCount) + } + } +} + +open class NoInterfaceTestActivity : AppCompatActivity() { + /** + * Displays the given [fragment] by adding it to a FragmentTransaction + */ + fun displayPreferenceFragment(fragment: PreferenceFragmentCompat) { + supportFragmentManager + .beginTransaction() + .replace(android.R.id.content, fragment) + .commitNow() + } +} + +class WithInterfaceTestActivity : NoInterfaceTestActivity(), + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, + PreferenceFragmentCompat.OnPreferenceStartScreenCallback, + PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback { + + private lateinit var testCallback: () -> Boolean + + /** + * Sets the [callback] to be invoked when an interface method from [PreferenceFragmentCompat] + * is invoked on this Activity. + * + * @param callback returns true if it handles the event, false if not + */ + fun setTestCallback(callback: () -> Boolean) { + testCallback = callback + } + + override fun onPreferenceStartFragment( + caller: PreferenceFragmentCompat?, + pref: Preference? + ) = testCallback() + + override fun onPreferenceStartScreen( + caller: PreferenceFragmentCompat?, + pref: PreferenceScreen? + ) = testCallback() + + override fun onPreferenceDisplayDialog( + caller: PreferenceFragmentCompat, + pref: Preference? + ) = testCallback() +} + +/** + * [Context] that implements interface methods so that we can return it from inside getContext(). + * + * @property testCallback invoked when an interface method from [PreferenceFragmentCompat] is + * invoked on this object. Returns true if it handles the event, false if not. + */ +private class TestContext(baseContext: Context, private val testCallback: () -> Boolean) : + ContextWrapper(baseContext), + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, + PreferenceFragmentCompat.OnPreferenceStartScreenCallback, + PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback { + + override fun onPreferenceStartFragment( + caller: PreferenceFragmentCompat?, + pref: Preference? + ) = testCallback() + + override fun onPreferenceStartScreen( + caller: PreferenceFragmentCompat?, + pref: PreferenceScreen? + ) = testCallback() + + override fun onPreferenceDisplayDialog( + caller: PreferenceFragmentCompat, + pref: Preference? + ) = testCallback() +} + +/** + * Testing fragment that will add the [Preference] created by [testPreferenceFactory] to the + * hierarchy, and set its title so it can be clicked on in tests. + * + * @property testPreferenceFactory factory that creates the [Preference] to be tested + * @property contextCallback optional callback that will be used in the interface methods + * implemented by a wrapped [TestContext] returned by [getContext] if not null. This simulates the + * case where a non-Activity object is returned by [getContext], with the interface methods + * implemented, for non-Activity based Fragment hosts. + */ +class TestFragment( + private val testPreferenceFactory: PreferenceFragmentCompat.() -> Preference, + private val contextCallback: (() -> Boolean)? = null +) : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceScreen = preferenceManager.createPreferenceScreen(context) + val testPreference = testPreferenceFactory() + testPreference.title = preferenceTitle + preferenceScreen.addPreference(testPreference) + } + + override fun getContext(): Context? { + val superContext = super.getContext()!! + return if (contextCallback != null) { + TestContext(superContext, contextCallback) + } else { + superContext + } + } + + companion object { + private const val preferenceTitle = "preference" + /** + * Asserts the preference created by [TestFragment.testPreferenceFactory] is displayed. + */ + fun assertPreferenceIsDisplayed() { + onView(withText(preferenceTitle)).check(matches(isDisplayed())) + } + /** + * Clicks on the preference created by [TestFragment.testPreferenceFactory]. + */ + fun clickOnPreference() { + onView(withText(preferenceTitle)).perform(click()) + } + } +} diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java b/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java index 1fa26698285..54b89e68ba3 100644 --- a/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java +++ b/preference/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java @@ -457,7 +457,7 @@ public abstract class PreferenceFragmentCompat extends Fragment implements .onPreferenceStartScreen(this, preferenceScreen); } if (!handled && getContext() instanceof OnPreferenceStartScreenCallback) { - ((OnPreferenceStartScreenCallback) getContext()) + handled = ((OnPreferenceStartScreenCallback) getContext()) .onPreferenceStartScreen(this, preferenceScreen); } // Check the Activity as well in case getContext was overridden to return something other |