Merge "Add fake dragging to ViewPager2" into androidx-master-dev
diff --git a/activity/api/1.0.0-alpha06.txt b/activity/api/1.0.0-alpha06.txt
index 1d5064e..c46e0cd 100644
--- a/activity/api/1.0.0-alpha06.txt
+++ b/activity/api/1.0.0-alpha06.txt
@@ -3,20 +3,27 @@
public class ComponentActivity extends androidx.core.app.ComponentActivity implements androidx.lifecycle.LifecycleOwner androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
ctor public ComponentActivity();
- method public void addOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
- method public void addOnBackPressedCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+ method @Deprecated public void addOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
+ method @Deprecated public void addOnBackPressedCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
method @Deprecated public Object? getLastCustomNonConfigurationInstance();
method public androidx.lifecycle.Lifecycle getLifecycle();
+ method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
method public final androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
method public androidx.lifecycle.ViewModelStore getViewModelStore();
method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
method public final Object? onRetainNonConfigurationInstance();
- method public void removeOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
+ method @Deprecated public void removeOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
}
public interface OnBackPressedCallback {
method public boolean handleOnBackPressed();
}
+ public final class OnBackPressedDispatcher {
+ method public androidx.arch.core.util.Cancellable addCallback(androidx.activity.OnBackPressedCallback);
+ method public androidx.arch.core.util.Cancellable addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+ method public boolean onBackPressed();
+ }
+
}
diff --git a/activity/api/current.txt b/activity/api/current.txt
index 1d5064e..c46e0cd 100644
--- a/activity/api/current.txt
+++ b/activity/api/current.txt
@@ -3,20 +3,27 @@
public class ComponentActivity extends androidx.core.app.ComponentActivity implements androidx.lifecycle.LifecycleOwner androidx.savedstate.SavedStateRegistryOwner androidx.lifecycle.ViewModelStoreOwner {
ctor public ComponentActivity();
- method public void addOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
- method public void addOnBackPressedCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+ method @Deprecated public void addOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
+ method @Deprecated public void addOnBackPressedCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
method @Deprecated public Object? getLastCustomNonConfigurationInstance();
method public androidx.lifecycle.Lifecycle getLifecycle();
+ method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
method public final androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
method public androidx.lifecycle.ViewModelStore getViewModelStore();
method @Deprecated public Object? onRetainCustomNonConfigurationInstance();
method public final Object? onRetainNonConfigurationInstance();
- method public void removeOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
+ method @Deprecated public void removeOnBackPressedCallback(androidx.activity.OnBackPressedCallback);
}
public interface OnBackPressedCallback {
method public boolean handleOnBackPressed();
}
+ public final class OnBackPressedDispatcher {
+ method public androidx.arch.core.util.Cancellable addCallback(androidx.activity.OnBackPressedCallback);
+ method public androidx.arch.core.util.Cancellable addCallback(androidx.lifecycle.LifecycleOwner, androidx.activity.OnBackPressedCallback);
+ method public boolean onBackPressed();
+ }
+
}
diff --git a/activity/build.gradle b/activity/build.gradle
index f687bc8..96270d9 100644
--- a/activity/build.gradle
+++ b/activity/build.gradle
@@ -15,6 +15,7 @@
dependencies {
api(project(":annotation"))
+ api(project(":arch:core-common"))
api(project(":core")) {
exclude group: 'androidx.annotation'
exclude group: 'com.google.guava', module: 'listenablefuture'
diff --git a/activity/src/androidTest/AndroidManifest.xml b/activity/src/androidTest/AndroidManifest.xml
index 9eb99d5..4d2f379 100644
--- a/activity/src/androidTest/AndroidManifest.xml
+++ b/activity/src/androidTest/AndroidManifest.xml
@@ -23,7 +23,6 @@
<activity android:name="androidx.activity.EagerOverrideLifecycleComponentActivity"/>
<activity android:name="androidx.activity.LazyOverrideLifecycleComponentActivity"/>
<activity android:name="androidx.activity.ViewModelActivity"/>
- <activity android:name="androidx.activity.OnBackPressedComponentActivity"/>
<activity android:name="androidx.activity.SavedStateActivity"/>
<activity android:name="androidx.activity.ContentViewActivity"/>
<activity android:name="androidx.activity.AutoRestarterActivity"/>
diff --git a/activity/src/androidTest/java/androidx/activity/ComponentActivityOnBackPressedTest.kt b/activity/src/androidTest/java/androidx/activity/ComponentActivityOnBackPressedTest.kt
deleted file mode 100644
index 396947f..0000000
--- a/activity/src/androidTest/java/androidx/activity/ComponentActivityOnBackPressedTest.kt
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright 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 androidx.activity
-
-import androidx.lifecycle.GenericLifecycleObserver
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
-import androidx.test.annotation.UiThreadTest
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import androidx.test.rule.ActivityTestRule
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
-import java.util.concurrent.CountDownLatch
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class ComponentActivityOnBackPressedTest {
-
- @get:Rule
- val activityRule = ActivityTestRule(OnBackPressedComponentActivity::class.java)
-
- @UiThreadTest
- @Test
- fun testAddOnBackPressedListener() {
- val activity = activityRule.activity
-
- val onBackPressedCallback = CountingOnBackPressedCallback()
-
- activity.addOnBackPressedCallback(onBackPressedCallback)
- activity.onBackPressed()
- assertWithMessage("Count should be incremented after handleOnBackPressed")
- .that(onBackPressedCallback.count)
- .isEqualTo(1)
- }
-
- @UiThreadTest
- @Test
- fun testRemoveOnBackPressedListener() {
- val activity = activityRule.activity
-
- val onBackPressedCallback = CountingOnBackPressedCallback()
-
- activity.addOnBackPressedCallback(onBackPressedCallback)
- activity.onBackPressed()
- assertWithMessage("Count should be incremented after handleOnBackPressed")
- .that(onBackPressedCallback.count)
- .isEqualTo(1)
-
- activity.removeOnBackPressedCallback(onBackPressedCallback)
- activity.onBackPressed()
- // Check that the count still equals 1
- assertWithMessage("Count shouldn't be incremented after removal")
- .that(onBackPressedCallback.count)
- .isEqualTo(1)
- }
-
- @UiThreadTest
- @Test
- fun testMultipleCalls() {
- val activity = activityRule.activity
-
- val onBackPressedCallback = CountingOnBackPressedCallback()
-
- activity.addOnBackPressedCallback(onBackPressedCallback)
- activity.onBackPressed()
- activity.onBackPressed()
- assertWithMessage("Count should be incremented after each handleOnBackPressed")
- .that(onBackPressedCallback.count)
- .isEqualTo(2)
- }
-
- @UiThreadTest
- @Test
- fun testMostRecentGetsPriority() {
- val activity = activityRule.activity
-
- val onBackPressedCallback = CountingOnBackPressedCallback()
- val mostRecentOnBackPressedCallback = CountingOnBackPressedCallback()
-
- activity.addOnBackPressedCallback(onBackPressedCallback)
- activity.addOnBackPressedCallback(mostRecentOnBackPressedCallback)
- activity.onBackPressed()
- assertWithMessage("Most recent callback should be incremented")
- .that(mostRecentOnBackPressedCallback.count)
- .isEqualTo(1)
- assertWithMessage("Only the most recent callback should be incremented")
- .that(onBackPressedCallback.count)
- .isEqualTo(0)
- }
-
- @UiThreadTest
- @Test
- fun testPassthroughListener() {
- val activity = activityRule.activity
-
- val onBackPressedCallback = CountingOnBackPressedCallback()
- val passThroughOnBackPressedCallback = CountingOnBackPressedCallback(returnValue = false)
-
- activity.addOnBackPressedCallback(onBackPressedCallback)
- activity.addOnBackPressedCallback(passThroughOnBackPressedCallback)
- activity.onBackPressed()
- assertWithMessage("Most recent callback should be incremented")
- .that(passThroughOnBackPressedCallback.count)
- .isEqualTo(1)
- assertWithMessage("Previous callbacks should be incremented if more recent callbacks " +
- "return false")
- .that(onBackPressedCallback.count)
- .isEqualTo(1)
- }
-
- @UiThreadTest
- @Test
- fun testLifecycleCallback() {
- val activity = activityRule.activity
-
- val onBackPressedCallback = CountingOnBackPressedCallback()
- val lifecycleOnBackPressedCallback = CountingOnBackPressedCallback()
- val lifecycleOwner = object : LifecycleOwner {
- val lifecycleRegistry = LifecycleRegistry(this)
-
- override fun getLifecycle() = lifecycleRegistry
- }
-
- activity.addOnBackPressedCallback(onBackPressedCallback)
- activity.addOnBackPressedCallback(lifecycleOwner, lifecycleOnBackPressedCallback)
- activity.onBackPressed()
- assertWithMessage("Non-started callbacks shouldn't have their count incremented")
- .that(lifecycleOnBackPressedCallback.count)
- .isEqualTo(0)
- assertWithMessage("Previous callbacks should be incremented if more recent callbacks " +
- "aren't started")
- .that(onBackPressedCallback.count)
- .isEqualTo(1)
-
- // Now start the Lifecycle
- lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
- activity.onBackPressed()
- assertWithMessage("Once the callbacks is started, the count should increment")
- .that(lifecycleOnBackPressedCallback.count)
- .isEqualTo(1)
- assertWithMessage("Only the most recent callback should be incremented")
- .that(onBackPressedCallback.count)
- .isEqualTo(1)
-
- // Now stop the Lifecycle
- lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
- activity.onBackPressed()
- assertWithMessage("Non-started callbacks shouldn't have their count incremented")
- .that(lifecycleOnBackPressedCallback.count)
- .isEqualTo(1)
- assertWithMessage("Previous callbacks should be incremented if more recent callbacks " +
- "aren't started")
- .that(onBackPressedCallback.count)
- .isEqualTo(2)
-
- // Now destroy the Lifecycle
- lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
- @Suppress("INACCESSIBLE_TYPE")
- assertWithMessage("onDestroy should trigger the removal of any associated callbacks")
- .that(activity.mOnBackPressedCallbacks)
- .hasSize(1)
- activity.onBackPressed()
- assertWithMessage("Non-started callbacks shouldn't have their count incremented")
- .that(lifecycleOnBackPressedCallback.count)
- .isEqualTo(1)
- assertWithMessage("Previous callbacks should be incremented if more recent callbacks " +
- "aren't started")
- .that(onBackPressedCallback.count)
- .isEqualTo(3)
- }
-}
-
-class CountingOnBackPressedCallback(val returnValue: Boolean = true) :
- OnBackPressedCallback {
- var count = 0
-
- override fun handleOnBackPressed(): Boolean {
- count++
- return returnValue
- }
-}
-
-class OnBackPressedComponentActivity : ComponentActivity() {
- val activityCallbackLifecycleOwner: LifecycleOwner = mock(LifecycleOwner::class.java)
- val lifecycleObserver: GenericLifecycleObserver = mock(GenericLifecycleObserver::class.java)
- val destroyCountDownLatch = CountDownLatch(1)
-
- init {
- lifecycle.addObserver(lifecycleObserver)
- }
-
- override fun onDestroy() {
- lifecycleObserver.onStateChanged(activityCallbackLifecycleOwner,
- Lifecycle.Event.ON_DESTROY)
- super.onDestroy()
- destroyCountDownLatch.countDown()
- }
-}
diff --git a/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
new file mode 100644
index 0000000..ca8fcdc
--- /dev/null
+++ b/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -0,0 +1,247 @@
+/*
+ * 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.activity
+
+import androidx.arch.core.util.Cancellable
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class OnBackPressedHandlerTest {
+
+ lateinit var dispatcher: OnBackPressedDispatcher
+
+ @Before
+ fun setup() {
+ dispatcher = OnBackPressedDispatcher()
+ }
+
+ @UiThreadTest
+ @Test
+ fun testAddCallback() {
+ val onBackPressedCallback = CountingOnBackPressedCallback()
+
+ dispatcher.addCallback(onBackPressedCallback)
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Count should be incremented after onBackPressed")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(1)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testCancelSubscription() {
+ val onBackPressedCallback = CountingOnBackPressedCallback()
+
+ val subscription = dispatcher.addCallback(onBackPressedCallback)
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Count should be incremented after onBackPressed")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(1)
+
+ subscription.cancel()
+ assertWithMessage("Cancellable should be cancelled after cancel()")
+ .that(subscription.isCancelled)
+ .isTrue()
+ assertWithMessage("Handler should return false when no OnBackPressedCallbacks " +
+ "are registered")
+ .that(dispatcher.onBackPressed())
+ .isFalse()
+ // Check that the count still equals 1
+ assertWithMessage("Count shouldn't be incremented after removal")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(1)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testMultipleCalls() {
+ val onBackPressedCallback = CountingOnBackPressedCallback()
+
+ dispatcher.addCallback(onBackPressedCallback)
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Count should be incremented after each onBackPressed")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(2)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testMostRecentGetsPriority() {
+ val onBackPressedCallback = CountingOnBackPressedCallback()
+ val mostRecentOnBackPressedCallback = CountingOnBackPressedCallback()
+
+ dispatcher.addCallback(onBackPressedCallback)
+ dispatcher.addCallback(mostRecentOnBackPressedCallback)
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Most recent callback should be incremented")
+ .that(mostRecentOnBackPressedCallback.count)
+ .isEqualTo(1)
+ assertWithMessage("Only the most recent callback should be incremented")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(0)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testPassthroughListener() {
+ val onBackPressedCallback = CountingOnBackPressedCallback()
+ val passThroughOnBackPressedCallback = CountingOnBackPressedCallback(returnValue = false)
+
+ dispatcher.addCallback(onBackPressedCallback)
+ dispatcher.addCallback(passThroughOnBackPressedCallback)
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Most recent callback should be incremented")
+ .that(passThroughOnBackPressedCallback.count)
+ .isEqualTo(1)
+ assertWithMessage("Previous callbacks should be incremented if more recent callbacks " +
+ "return false")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(1)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testLifecycleCallback() {
+ val onBackPressedCallback = CountingOnBackPressedCallback()
+ val lifecycleOnBackPressedCallback = CountingOnBackPressedCallback()
+ val lifecycleOwner = object : LifecycleOwner {
+ val lifecycleRegistry = LifecycleRegistry(this)
+
+ override fun getLifecycle() = lifecycleRegistry
+ }
+
+ dispatcher.addCallback(onBackPressedCallback)
+ val observeSubscription = dispatcher.addCallback(lifecycleOwner,
+ lifecycleOnBackPressedCallback)
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Non-started callbacks shouldn't have their count incremented")
+ .that(lifecycleOnBackPressedCallback.count)
+ .isEqualTo(0)
+ assertWithMessage("Previous callbacks should be incremented if more recent callbacks " +
+ "aren't started")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(1)
+
+ // Now start the Lifecycle
+ lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Once the callbacks is started, the count should increment")
+ .that(lifecycleOnBackPressedCallback.count)
+ .isEqualTo(1)
+ assertWithMessage("Only the most recent callback should be incremented")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(1)
+
+ // Now stop the Lifecycle
+ lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Non-started callbacks shouldn't have their count incremented")
+ .that(lifecycleOnBackPressedCallback.count)
+ .isEqualTo(1)
+ assertWithMessage("Previous callbacks should be incremented if more recent callbacks " +
+ "aren't started")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(2)
+
+ // Now destroy the Lifecycle
+ lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ @Suppress("INACCESSIBLE_TYPE")
+ assertWithMessage("onDestroy should trigger the removal of any associated callbacks")
+ .that(observeSubscription.isCancelled)
+ .isTrue()
+ assertWithMessage("Handler should return true when handling onBackPressed")
+ .that(dispatcher.onBackPressed())
+ .isTrue()
+ assertWithMessage("Non-started callbacks shouldn't have their count incremented")
+ .that(lifecycleOnBackPressedCallback.count)
+ .isEqualTo(1)
+ assertWithMessage("Previous callbacks should be incremented if more recent callbacks " +
+ "aren't started")
+ .that(onBackPressedCallback.count)
+ .isEqualTo(3)
+ }
+
+ @UiThreadTest
+ @Test
+ fun testLifecycleCallback_whenDestroyed() {
+ val lifecycleOnBackPressedCallback = CountingOnBackPressedCallback()
+ val lifecycleOwner = object : LifecycleOwner {
+ val lifecycleRegistry = LifecycleRegistry(this)
+
+ override fun getLifecycle() = lifecycleRegistry
+ }
+ // Start the Lifecycle as DESTROYED
+ lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+
+ val subscription = dispatcher.addCallback(lifecycleOwner,
+ lifecycleOnBackPressedCallback)
+ assertWithMessage("Dispatcher should return Cancellable.CANCELLED if the Lifecycle is " +
+ "DESTROYED")
+ .that(subscription)
+ .isSameAs(Cancellable.CANCELLED)
+ assertWithMessage("Cancellable should be immediately cancelled if the Lifecycle is " +
+ "DESTROYED")
+ .that(subscription.isCancelled)
+ .isTrue()
+ assertWithMessage("Handler should return false when no OnBackPressedCallbacks " +
+ "are registered")
+ .that(dispatcher.onBackPressed())
+ .isFalse()
+ assertWithMessage("Count shouldn't be incremented when the Cancellable is cancelled")
+ .that(lifecycleOnBackPressedCallback.count)
+ .isEqualTo(0)
+ }
+}
+
+class CountingOnBackPressedCallback(val returnValue: Boolean = true) :
+ OnBackPressedCallback {
+ var count = 0
+
+ override fun handleOnBackPressed(): Boolean {
+ count++
+ return returnValue
+ }
+}
diff --git a/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/src/main/java/androidx/activity/ComponentActivity.java
index a3bbef7..1619920 100644
--- a/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -27,6 +27,7 @@
import androidx.annotation.ContentView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.arch.core.util.Cancellable;
import androidx.lifecycle.GenericLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
@@ -39,8 +40,7 @@
import androidx.savedstate.SavedStateRegistryOwner;
import java.util.HashMap;
-import java.util.Iterator;
-import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.WeakHashMap;
/**
* Base class for activities that enables composition of higher level components.
@@ -66,9 +66,12 @@
// Lazily recreated from NonConfigurationInstances by getViewModelStore()
private ViewModelStore mViewModelStore;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- final CopyOnWriteArrayList<LifecycleAwareOnBackPressedCallback> mOnBackPressedCallbacks =
- new CopyOnWriteArrayList<>();
+ private final OnBackPressedDispatcher mOnBackPressedDispatcher = new OnBackPressedDispatcher();
+ /**
+ * Used for the deprecated {@link #removeOnBackPressedCallback(OnBackPressedCallback)}.
+ */
+ private final WeakHashMap<OnBackPressedCallback, Cancellable>
+ mOnBackPressedCallbackCancellables = new WeakHashMap<>();
// Cache the ContentView layoutIds for Activities.
private static final HashMap<Class, Integer> sAnnotationIds = new HashMap<>();
@@ -258,26 +261,33 @@
/**
* Called when the activity has detected the user's press of the back
- * key. Any {@link OnBackPressedCallback} added via
- * {@link #addOnBackPressedCallback(LifecycleOwner, OnBackPressedCallback)} will be given a
+ * key. The {@link #getOnBackPressedDispatcher() OnBackPressedDispatcher} will be given a
* chance to handle the back button before the default behavior of
* {@link android.app.Activity#onBackPressed()} is invoked.
*
- * @see #addOnBackPressedCallback(LifecycleOwner, OnBackPressedCallback)
+ * @see #getOnBackPressedDispatcher()
*/
@Override
public void onBackPressed() {
- for (OnBackPressedCallback onBackPressedCallback : mOnBackPressedCallbacks) {
- if (onBackPressedCallback.handleOnBackPressed()) {
- return;
- }
+ if (mOnBackPressedDispatcher.onBackPressed()) {
+ return;
}
- // If none of the registered OnBackPressedCallbacks handled the back button,
+ // If the OnBackPressedDispatcher doesn't handle the back button,
// delegate to the super implementation
super.onBackPressed();
}
/**
+ * Retrieve the {@link OnBackPressedDispatcher} that will be triggered when
+ * {@link #onBackPressed()} is called.
+ * @return The {@link OnBackPressedDispatcher} associated with this ComponentActivity.
+ */
+ @NonNull
+ public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
+ return mOnBackPressedDispatcher;
+ }
+
+ /**
* Add a new {@link OnBackPressedCallback}. Callbacks are invoked in order of recency, so
* this newly added {@link OnBackPressedCallback} will be the first callback to receive a
* callback if {@link #onBackPressed()} is called. Only if this callback returns
@@ -295,9 +305,15 @@
*
* @see #onBackPressed()
* @see #removeOnBackPressedCallback(OnBackPressedCallback)
+ * @deprecated Use {@link #getOnBackPressedDispatcher() and
+ * {@link OnBackPressedDispatcher#addCallback(LifecycleOwner, OnBackPressedCallback)}},
+ * explicitly passing in this Activity object as the {@link LifecycleOwner}.
*/
+ @Deprecated
public void addOnBackPressedCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
- addOnBackPressedCallback(this, onBackPressedCallback);
+ mOnBackPressedCallbackCancellables.put(onBackPressedCallback,
+ getOnBackPressedDispatcher()
+ .addCallback(this, onBackPressedCallback));
}
/**
@@ -319,18 +335,15 @@
*
* @see #onBackPressed()
* @see #removeOnBackPressedCallback(OnBackPressedCallback)
+ * @deprecated Use {@link #getOnBackPressedDispatcher() and
+ * {@link OnBackPressedDispatcher#addCallback(LifecycleOwner, OnBackPressedCallback)}}.
*/
+ @Deprecated
public void addOnBackPressedCallback(@NonNull LifecycleOwner owner,
@NonNull OnBackPressedCallback onBackPressedCallback) {
- Lifecycle lifecycle = owner.getLifecycle();
- if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
- // Already destroyed, nothing to do
- return;
- }
- // Add new callbacks to the front of the list so that
- // the most recently added callbacks get priority
- mOnBackPressedCallbacks.add(0, new LifecycleAwareOnBackPressedCallback(
- lifecycle, onBackPressedCallback));
+ mOnBackPressedCallbackCancellables.put(onBackPressedCallback,
+ getOnBackPressedDispatcher()
+ .addCallback(owner, onBackPressedCallback));
}
/**
@@ -345,21 +358,16 @@
*
* @param onBackPressedCallback The callback to remove
* @see #addOnBackPressedCallback(LifecycleOwner, OnBackPressedCallback)
+ * @deprecated Use {@link Cancellable#cancel()} on the
+ * {@link Cancellable} returned by {@link #getOnBackPressedDispatcher() and
+ * {@link OnBackPressedDispatcher#addCallback }}.
*/
+ @SuppressWarnings("DeprecatedIsStillUsed") /* See mOnBackPressedCallbackCancellables */
+ @Deprecated
public void removeOnBackPressedCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
- Iterator<LifecycleAwareOnBackPressedCallback> iterator =
- mOnBackPressedCallbacks.iterator();
- LifecycleAwareOnBackPressedCallback callbackToRemove = null;
- while (iterator.hasNext()) {
- LifecycleAwareOnBackPressedCallback callback = iterator.next();
- if (callback.getOnBackPressedCallback().equals(onBackPressedCallback)) {
- callbackToRemove = callback;
- break;
- }
- }
- if (callbackToRemove != null) {
- callbackToRemove.onRemoved();
- mOnBackPressedCallbacks.remove(callbackToRemove);
+ Cancellable cancellable = mOnBackPressedCallbackCancellables.remove(onBackPressedCallback);
+ if (cancellable != null) {
+ cancellable.cancel();
}
}
@@ -368,48 +376,4 @@
public final SavedStateRegistry getSavedStateRegistry() {
return mSavedStateRegistryController.getSavedStateRegistry();
}
-
- private class LifecycleAwareOnBackPressedCallback implements
- OnBackPressedCallback,
- GenericLifecycleObserver {
- private final Lifecycle mLifecycle;
- private final OnBackPressedCallback mOnBackPressedCallback;
-
- LifecycleAwareOnBackPressedCallback(@NonNull Lifecycle lifecycle,
- @NonNull OnBackPressedCallback onBackPressedCallback) {
- mLifecycle = lifecycle;
- mOnBackPressedCallback = onBackPressedCallback;
- mLifecycle.addObserver(this);
- }
-
- Lifecycle getLifecycle() {
- return mLifecycle;
- }
-
- OnBackPressedCallback getOnBackPressedCallback() {
- return mOnBackPressedCallback;
- }
-
- @Override
- public boolean handleOnBackPressed() {
- if (mLifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
- return mOnBackPressedCallback.handleOnBackPressed();
- }
- return false;
- }
-
- @Override
- public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
- if (event == Lifecycle.Event.ON_DESTROY) {
- synchronized (mOnBackPressedCallbacks) {
- mLifecycle.removeObserver(this);
- mOnBackPressedCallbacks.remove(this);
- }
- }
- }
-
- public void onRemoved() {
- mLifecycle.removeObserver(this);
- }
- }
}
diff --git a/activity/src/main/java/androidx/activity/OnBackPressedCallback.java b/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
index 3eed965..fff6476 100644
--- a/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
+++ b/activity/src/main/java/androidx/activity/OnBackPressedCallback.java
@@ -16,20 +16,17 @@
package androidx.activity;
-import androidx.lifecycle.LifecycleOwner;
-
/**
- * Interface for handling {@link ComponentActivity#onBackPressed()} callbacks without
+ * Interface for handling {@link OnBackPressedDispatcher#onBackPressed()} callbacks without
* strongly coupling that implementation to a subclass of {@link ComponentActivity}.
*
- * @see ComponentActivity#addOnBackPressedCallback(LifecycleOwner, OnBackPressedCallback)
- * @see ComponentActivity#removeOnBackPressedCallback(OnBackPressedCallback)
+ * @see ComponentActivity#getOnBackPressedDispatcher()
*/
public interface OnBackPressedCallback {
/**
- * Callback for handling the {@link ComponentActivity#onBackPressed()} event.
+ * Callback for handling the {@link OnBackPressedDispatcher#onBackPressed()} event.
*
- * @return True if you handled the {@link ComponentActivity#onBackPressed()} event. No
+ * @return True if you handled the {@link OnBackPressedDispatcher#onBackPressed()} event. No
* further {@link OnBackPressedCallback} instances will be called if you return true.
*/
boolean handleOnBackPressed();
diff --git a/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java b/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
new file mode 100644
index 0000000..b11e8d2
--- /dev/null
+++ b/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.java
@@ -0,0 +1,212 @@
+/*
+ * 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.activity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.util.Cancellable;
+import androidx.lifecycle.GenericLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.ArrayDeque;
+import java.util.Iterator;
+
+/**
+ * Dispatcher that can be used to register {@link OnBackPressedCallback} instances for handling
+ * the {@link ComponentActivity#onBackPressed()} callback via composition.
+ * <pre>
+ * public class FormEntryFragment extends Fragment {
+ * {@literal @}Override
+ * public void onAttach({@literal @}NonNull Context context) {
+ * super.onAttach(context);
+ * requireActivity().getOnBackPressedDispatcher().addCallback(this,
+ * new OnBackPressedCallback() {
+ * {@literal @}Override
+ * public boolean handleOnBackPressed() {
+ * showAreYouSureDialog();
+ * return true;
+ * }
+ * });
+ * }
+ * }
+ * </pre>
+ */
+public final class OnBackPressedDispatcher {
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();
+
+ OnBackPressedDispatcher() {
+ }
+
+ /**
+ * Add a new {@link OnBackPressedCallback}. Callbacks are invoked in the reverse order in which
+ * they are added, so this newly added {@link OnBackPressedCallback} will be the first
+ * callback to receive a callback if {@link #onBackPressed()} is called.
+ * <p>
+ * This method is <strong>not</strong> {@link Lifecycle} aware - if you'd like to ensure that
+ * you only get callbacks when at least {@link Lifecycle.State#STARTED started}, use
+ * {@link #addCallback(LifecycleOwner, OnBackPressedCallback)}.
+ *
+ * @param onBackPressedCallback The callback to add
+ * @return a {@link Cancellable} which can be used to {@link Cancellable#cancel() cancel}
+ * the callback and remove it from the set of OnBackPressedCallbacks. The callback won't be
+ * called for any future {@link #onBackPressed()} calls, but may still receive a
+ * callback if {@link Cancellable#cancel()} is called during the dispatch of an ongoing
+ * {@link #onBackPressed()} call.
+ *
+ * @see #onBackPressed()
+ */
+ @NonNull
+ public Cancellable addCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
+ synchronized (mOnBackPressedCallbacks) {
+ mOnBackPressedCallbacks.add(onBackPressedCallback);
+ }
+ return new OnBackPressedCancellable(onBackPressedCallback);
+ }
+
+ /**
+ * Receive callbacks to a new {@link OnBackPressedCallback} when the given
+ * {@link LifecycleOwner} is at least {@link Lifecycle.State#STARTED started}.
+ * <p>
+ * This will automatically call {@link #addCallback(OnBackPressedCallback)} and
+ * {@link Cancellable#cancel()} as the lifecycle state changes.
+ * As a corollary, if your lifecycle is already at least
+ * {@link Lifecycle.State#STARTED started}, calling this method will result in an immediate
+ * call to {@link #addCallback(OnBackPressedCallback)}.
+ * <p>
+ * When the {@link LifecycleOwner} is {@link Lifecycle.State#DESTROYED destroyed}, it will
+ * automatically be removed from the list of callbacks. The only time you would need to
+ * manually call {@link Cancellable#cancel()} on the returned {@link Cancellable} is if
+ * you'd like to remove the callback prior to destruction of the associated lifecycle.
+ *
+ * <p>If the Lifecycle is already
+ * {@link Lifecycle.State#DESTROYED destroyed} when this method is called, this will
+ * return{@link Cancellable#CANCELLED} and the callback will not be added.
+ *
+ * @param owner The LifecycleOwner which controls when the callback should be invoked
+ * @param onBackPressedCallback The callback to add
+ * @return a {@link Cancellable} which can be used to {@link Cancellable#cancel() cancel}
+ * the callback and remove the associated {@link androidx.lifecycle.LifecycleObserver}
+ * and the OnBackPressedCallback. The callback won't be called for any future
+ * {@link #onBackPressed()} calls, but may still receive a callback if
+ * {@link Cancellable#cancel()} is called during the dispatch of an ongoing
+ * {@link #onBackPressed()} call.
+ *
+ * @see #onBackPressed()
+ */
+ @NonNull
+ public Cancellable addCallback(@NonNull LifecycleOwner owner,
+ @NonNull OnBackPressedCallback onBackPressedCallback) {
+ Lifecycle lifecycle = owner.getLifecycle();
+ if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
+ return Cancellable.CANCELLED;
+ }
+ return new LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback);
+ }
+
+ /**
+ * Trigger a call to the currently added {@link OnBackPressedCallback callbacks} in reverse
+ * order in which they were added. Only if the most recently added callback returns
+ * <code>false</code> from its {@link OnBackPressedCallback#handleOnBackPressed()}
+ * will any previously added callback be called.
+ *
+ * @return True if an added {@link OnBackPressedCallback} handled the back button.
+ */
+ public boolean onBackPressed() {
+ synchronized (mOnBackPressedCallbacks) {
+ Iterator<OnBackPressedCallback> iterator =
+ mOnBackPressedCallbacks.descendingIterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().handleOnBackPressed()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private class OnBackPressedCancellable implements Cancellable {
+ private final OnBackPressedCallback mOnBackPressedCallback;
+ private boolean mCancelled;
+
+ OnBackPressedCancellable(OnBackPressedCallback onBackPressedCallback) {
+ mOnBackPressedCallback = onBackPressedCallback;
+ }
+
+ @Override
+ public void cancel() {
+ synchronized (mOnBackPressedCallbacks) {
+ mOnBackPressedCallbacks.remove(mOnBackPressedCallback);
+ mCancelled = true;
+ }
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return mCancelled;
+ }
+ }
+
+ private class LifecycleOnBackPressedCancellable implements GenericLifecycleObserver,
+ Cancellable {
+ private final Lifecycle mLifecycle;
+ private final OnBackPressedCallback mOnBackPressedCallback;
+
+ @Nullable
+ private Cancellable mCurrentCancellable;
+ private boolean mCancelled = false;
+
+ LifecycleOnBackPressedCancellable(@NonNull Lifecycle lifecycle,
+ @NonNull OnBackPressedCallback onBackPressedCallback) {
+ mLifecycle = lifecycle;
+ mOnBackPressedCallback = onBackPressedCallback;
+ lifecycle.addObserver(this);
+ }
+
+ @Override
+ public void onStateChanged(@NonNull LifecycleOwner source,
+ @NonNull Lifecycle.Event event) {
+ if (event == Lifecycle.Event.ON_START) {
+ mCurrentCancellable = addCallback(mOnBackPressedCallback);
+ } else if (event == Lifecycle.Event.ON_STOP) {
+ // Should always be non-null
+ if (mCurrentCancellable != null) {
+ mCurrentCancellable.cancel();
+ }
+ } else if (event == Lifecycle.Event.ON_DESTROY) {
+ cancel();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ mLifecycle.removeObserver(this);
+ if (mCurrentCancellable != null) {
+ mCurrentCancellable.cancel();
+ mCurrentCancellable = null;
+ }
+ mCancelled = true;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return mCancelled;
+ }
+ }
+}
diff --git a/appcompat/resources/build.gradle b/appcompat/resources/build.gradle
index ebdf4d3..b78317e 100644
--- a/appcompat/resources/build.gradle
+++ b/appcompat/resources/build.gradle
@@ -25,11 +25,10 @@
dependencies {
api(project(":annotation"))
-
api("androidx.core:core:1.0.1")
implementation("androidx.collection:collection:1.0.0")
- api("androidx.vectordrawable:vectordrawable:1.0.1")
- api("androidx.vectordrawable:vectordrawable-animated:1.0.0")
+ api(project(":vectordrawable"))
+ api(project(":vectordrawable-animated"))
androidTestImplementation(TEST_EXT_JUNIT)
androidTestImplementation(TEST_CORE)
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
index d4267b0..f9695e0 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatSpinnerTest.java
@@ -19,8 +19,10 @@
import static androidx.appcompat.testutils.TestUtilsMatchers.isCombinedBackground;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isPlatformPopup;
+import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@@ -32,6 +34,7 @@
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.os.SystemClock;
+import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
@@ -40,6 +43,11 @@
import androidx.appcompat.test.R;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
+import androidx.test.espresso.ViewAction;
+import androidx.test.espresso.action.CoordinatesProvider;
+import androidx.test.espresso.action.GeneralSwipeAction;
+import androidx.test.espresso.action.Press;
+import androidx.test.espresso.action.Swipe;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
@@ -186,4 +194,74 @@
mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
onView(withText(EARTH)).check(matches(isDisplayed()));
}
+
+ @LargeTest
+ @Test
+ public void testSlowScroll() {
+ onView(withId(R.id.spinner_dropdown_popup_with_scroll)).perform(click());
+
+ final AppCompatSpinner spinner = mContainer
+ .findViewById(R.id.spinner_dropdown_popup_with_scroll);
+ String secondItem = (String) spinner.getAdapter().getItem(1);
+
+ onView(isAssignableFrom(DropDownListView.class)).perform(slowScrollPopup());
+
+ // when we scroll slowly a second time the popup list might jump back to the first element
+ onView(isAssignableFrom(DropDownListView.class)).perform(slowScrollPopup());
+
+ // because we scroll twice with one element height each,
+ // the second item should not be visible
+ onView(withText(secondItem))
+ .check(doesNotExist());
+ }
+
+ private ViewAction slowScrollPopup() {
+ return new GeneralSwipeAction(Swipe.SLOW,
+ new CoordinatesProvider() {
+ @Override
+ public float[] calculateCoordinates(View view) {
+ final float[] middleLocation = getViewMiddleLocation(view);
+ return new float[] {
+ middleLocation[0],
+ middleLocation[1]
+ };
+ }
+ },
+ new CoordinatesProvider() {
+ @Override
+ public float[] calculateCoordinates(View view) {
+ final float[] middleLocation = getViewMiddleLocation(view);
+ return new float[] {
+ middleLocation[0],
+ middleLocation[1] - getElementSize(view)
+ };
+ }
+ },
+ Press.PINPOINT
+ );
+ }
+
+ private float[] getViewMiddleLocation(View view) {
+ final DropDownListView list = (DropDownListView) view;
+
+ final int[] location = new int[2];
+ list.getLocationOnScreen(location);
+
+ final float x = location[0] + list.getWidth() / 2f;
+ final float y = location[1] + list.getHeight() / 2f;
+
+ return new float[] {x, y};
+ }
+
+ private int getElementSize(View view) {
+ final DropDownListView list = (DropDownListView) view;
+
+ final View child = list.getChildAt(0);
+ final int[] location = new int[2];
+ child.getLocationOnScreen(location);
+
+ // espresso doesn't actually scroll for the full amount specified
+ // so we add a little bit more to be safe
+ return child.getHeight() * 2;
+ }
}
diff --git a/appcompat/src/androidTest/java/androidx/appcompat/widget/ToolbarTest.java b/appcompat/src/androidTest/java/androidx/appcompat/widget/ToolbarTest.java
index 047a560..eb6ee0e 100644
--- a/appcompat/src/androidTest/java/androidx/appcompat/widget/ToolbarTest.java
+++ b/appcompat/src/androidTest/java/androidx/appcompat/widget/ToolbarTest.java
@@ -32,7 +32,6 @@
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.test.R;
import androidx.appcompat.testutils.TestUtils;
-import androidx.test.espresso.Espresso;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -155,18 +154,9 @@
}
@Test
- public void testToolbarOverflowIconWithThemedCSL() throws Throwable {
+ public void testToolbarOverflowIconWithThemedCSL() {
final Toolbar toolbar = mActivity.findViewById(R.id.toolbar_themedcsl_colorcontrolnormal);
- // Inflate a menu so that the overflow is displayed
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- toolbar.inflateMenu(R.menu.popup_menu);
- }
- });
- Espresso.onIdle();
-
// Assert that the overflow icon is tinted magenta, as per the theme
final Drawable icon = toolbar.getOverflowIcon();
assertNotNull(icon);
diff --git a/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml b/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
index da55bdf..38228a9 100644
--- a/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
+++ b/appcompat/src/androidTest/res/layout/appcompat_spinner_activity.xml
@@ -93,6 +93,13 @@
android:layout_height="wrap_content"
android:entries="@array/planets_array"
android:spinnerMode="dropdown" />
+
+ <androidx.appcompat.widget.AppCompatSpinner
+ android:id="@+id/spinner_dropdown_popup_with_scroll"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:entries="@array/numbers_array"
+ android:spinnerMode="dropdown" />
</LinearLayout>
</ScrollView>
diff --git a/appcompat/src/androidTest/res/layout/appcompat_toolbar_activity.xml b/appcompat/src/androidTest/res/layout/appcompat_toolbar_activity.xml
index c1bab6a..0d11cab 100644
--- a/appcompat/src/androidTest/res/layout/appcompat_toolbar_activity.xml
+++ b/appcompat/src/androidTest/res/layout/appcompat_toolbar_activity.xml
@@ -56,6 +56,7 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.ThemedCslMagenta"
+ app:menu="@menu/popup_menu"
app:subtitle="Subtitle"
app:title="Title" />
diff --git a/appcompat/src/androidTest/res/values/strings.xml b/appcompat/src/androidTest/res/values/strings.xml
index 964c32b..0188024 100644
--- a/appcompat/src/androidTest/res/values/strings.xml
+++ b/appcompat/src/androidTest/res/values/strings.xml
@@ -78,6 +78,48 @@
<item>Neptune</item>
<item>Pluto</item>
</string-array>
+ <string-array name="numbers_array">
+ <item>0</item>
+ <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>
+ <item>11</item>
+ <item>12</item>
+ <item>13</item>
+ <item>14</item>
+ <item>15</item>
+ <item>16</item>
+ <item>17</item>
+ <item>18</item>
+ <item>19</item>
+ <item>20</item>
+ <item>21</item>
+ <item>22</item>
+ <item>23</item>
+ <item>24</item>
+ <item>25</item>
+ <item>26</item>
+ <item>27</item>
+ <item>28</item>
+ <item>29</item>
+ <item>30</item>
+ <item>31</item>
+ <item>32</item>
+ <item>33</item>
+ <item>34</item>
+ <item>35</item>
+ <item>36</item>
+ <item>37</item>
+ <item>38</item>
+ <item>39</item>
+ </string-array>
<string name="night_mode">DAY</string>
diff --git a/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java b/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
index 86886bd..1033e46 100644
--- a/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
+++ b/appcompat/src/main/java/androidx/appcompat/widget/AppCompatSpinner.java
@@ -1015,12 +1015,6 @@
}
@Override
- @SuppressLint("SyntheticAccessor")
- public void show() {
- showPopup();
- }
-
- @Override
public void show(int textDirection, int textAlignment) {
final boolean wasShowing = isShowing();
diff --git a/arch/core-common/api/2.1.0-alpha01.txt b/arch/core-common/api/2.1.0-alpha01.txt
index dba1d6a7..4068f2c 100644
--- a/arch/core-common/api/2.1.0-alpha01.txt
+++ b/arch/core-common/api/2.1.0-alpha01.txt
@@ -1,6 +1,12 @@
// Signature format: 3.0
package androidx.arch.core.util {
+ public interface Cancellable {
+ method public void cancel();
+ method public boolean isCancelled();
+ field public static final androidx.arch.core.util.Cancellable CANCELLED;
+ }
+
public interface Function<I, O> {
method public O! apply(I!);
}
diff --git a/arch/core-common/api/current.txt b/arch/core-common/api/current.txt
index dba1d6a7..4068f2c 100644
--- a/arch/core-common/api/current.txt
+++ b/arch/core-common/api/current.txt
@@ -1,6 +1,12 @@
// Signature format: 3.0
package androidx.arch.core.util {
+ public interface Cancellable {
+ method public void cancel();
+ method public boolean isCancelled();
+ field public static final androidx.arch.core.util.Cancellable CANCELLED;
+ }
+
public interface Function<I, O> {
method public O! apply(I!);
}
diff --git a/arch/core-common/src/main/java/androidx/arch/core/util/Cancellable.java b/arch/core-common/src/main/java/androidx/arch/core/util/Cancellable.java
new file mode 100644
index 0000000..232f208
--- /dev/null
+++ b/arch/core-common/src/main/java/androidx/arch/core/util/Cancellable.java
@@ -0,0 +1,55 @@
+/*
+ * 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.arch.core.util;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Token representing a cancellable operation.
+ */
+public interface Cancellable {
+ /**
+ * An instance of Cancellable that is always cancelled - i.e., {@link #isCancelled()} will
+ * always return true.
+ */
+ @NonNull
+ Cancellable CANCELLED = new Cancellable() {
+ @Override
+ public void cancel() {
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return true;
+ }
+ };
+
+ /**
+ * Cancel the subscription. This call should be idempotent, making it safe to
+ * call multiple times.
+ */
+ void cancel();
+
+ /**
+ * Returns true if the subscription has been cancelled. This is inherently a
+ * racy operation if you are calling {@link #cancel()} on another thread, so this
+ * should be treated as a 'best effort' signal.
+ *
+ * @return Whether the subscription has been cancelled.
+ */
+ boolean isCancelled();
+}
diff --git a/biometric/res/values/strings.xml b/biometric/res/values/strings.xml
index e961bcb..1ee1534 100644
--- a/biometric/res/values/strings.xml
+++ b/biometric/res/values/strings.xml
@@ -32,6 +32,8 @@
<string name="fingerprint_error_hw_not_present">This device does not have a fingerprint sensor</string>
<!-- Generic error message shown when the fingerprint authentication operation is canceled due to user input. Generally not shown to the user. [CHAR LIMIT=NONE] -->
<string name="fingerprint_error_user_canceled">Fingerprint operation canceled by user.</string>
+ <!-- Error message shown after too many failed authentication attempts. [CHAR LIMIT=NONE] -->
+ <string name="fingerprint_error_lockout">Too many attempts. Please try again later.</string>
<!-- Generic error message shown when an unknown error has occurred. [CHAR LIMIT=NONE] -->
<string name="default_error_msg">Unknown error</string>
</resources>
diff --git a/biometric/src/main/java/androidx/biometric/BiometricPrompt.java b/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
index da6bf70..efdd360 100644
--- a/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
+++ b/biometric/src/main/java/androidx/biometric/BiometricPrompt.java
@@ -21,6 +21,7 @@
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
@@ -52,6 +53,9 @@
private static final String TAG = "BiometricPromptCompat";
private static final boolean DEBUG = false;
+ // In order to keep consistent behavior between versions, we need to send
+ // FingerprintDialogFragment a message indicating whether or not to dismiss the UI instantly.
+ private static final int DELAY_MILLIS = 500;
static final String DIALOG_FRAGMENT_TAG = "FingerprintDialogFragment";
static final String FINGERPRINT_HELPER_FRAGMENT_TAG = "FingerprintHelperFragment";
@@ -499,8 +503,12 @@
mFingerprintHelperFragment = FingerprintHelperFragment.newInstance();
}
mFingerprintHelperFragment.setCallback(mExecutor, mAuthenticationCallback);
- mFingerprintHelperFragment.setHandler(mFingerprintDialogFragment.getHandler());
+ final Handler fingerprintDialogHandler = mFingerprintDialogFragment.getHandler();
+ mFingerprintHelperFragment.setHandler(fingerprintDialogHandler);
mFingerprintHelperFragment.setCryptoObject(crypto);
+ fingerprintDialogHandler.sendMessageDelayed(
+ fingerprintDialogHandler.obtainMessage(
+ FingerprintDialogFragment.DISPLAYED_FOR_500_MS), DELAY_MILLIS);
if (fragmentManager.findFragmentByTag(FINGERPRINT_HELPER_FRAGMENT_TAG) == null) {
// If the fragment hasn't been added before, add it. It will also start the
diff --git a/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java b/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
index 763be0a..c8354ca 100644
--- a/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
+++ b/biometric/src/main/java/androidx/biometric/FingerprintDialogFragment.java
@@ -65,9 +65,14 @@
// Show an error in the help area, and dismiss the dialog afterwards
protected static final int MSG_SHOW_ERROR = 2;
// Dismisses the authentication dialog
- protected static final int MSG_DISMISS_DIALOG = 3;
+ protected static final int MSG_DISMISS_DIALOG_ERROR = 3;
// Resets the help message
protected static final int MSG_RESET_MESSAGE = 4;
+ // Dismisses the authentication dialog after success.
+ protected static final int MSG_DISMISS_DIALOG_AUTHENTICATED = 5;
+ // The amount of time required that this fragment be displayed for in order that
+ // we show an error message on top of the UI.
+ protected static final int DISPLAYED_FOR_500_MS = 6;
// States for icon animation
private static final int STATE_NONE = 0;
@@ -93,12 +98,18 @@
case MSG_SHOW_ERROR:
handleShowError(msg.arg1, (CharSequence) msg.obj);
break;
- case MSG_DISMISS_DIALOG:
- handleDismissDialog();
+ case MSG_DISMISS_DIALOG_ERROR:
+ handleDismissDialogError();
+ break;
+ case MSG_DISMISS_DIALOG_AUTHENTICATED:
+ dismiss();
break;
case MSG_RESET_MESSAGE:
handleResetMessage();
break;
+ case DISPLAYED_FOR_500_MS:
+ mDismissInstantly = false;
+ break;
}
}
}
@@ -113,6 +124,13 @@
private Context mContext;
private Dialog mDialog;
+ /**
+ * This flag is used to control the instant dismissal of the dialog fragment. In the case where
+ * the user is already locked out this dialog will not appear. In the case where the user is
+ * being locked out for the first time an error message will be displayed on the UI before
+ * dismissing.
+ */
+ protected boolean mDismissInstantly = true;
// This should be re-set by the BiometricPromptCompat each time the lifecycle changes.
DialogInterface.OnClickListener mNegativeButtonListener;
@@ -328,11 +346,31 @@
mErrorText.setText(msg);
// Dismiss the dialog after a delay
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DISMISS_DIALOG), HIDE_DIALOG_DELAY);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DISMISS_DIALOG_ERROR),
+ HIDE_DIALOG_DELAY);
}
- void handleDismissDialog() {
- dismiss();
+ void dismissAfterDelay() {
+ mErrorText.setTextColor(mErrorColor);
+ mErrorText.setText(
+ R.string.fingerprint_error_lockout);
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ dismiss();
+ }
+ }, HIDE_DIALOG_DELAY);
+ }
+
+ void handleDismissDialogError() {
+ if (mDismissInstantly) {
+ dismiss();
+ } else {
+ dismissAfterDelay();
+ }
+ // Always set this to true. In case the user tries to authenticate again the UI will not be
+ // shown.
+ mDismissInstantly = true;
}
void handleResetMessage() {
diff --git a/biometric/src/main/java/androidx/biometric/FingerprintHelperFragment.java b/biometric/src/main/java/androidx/biometric/FingerprintHelperFragment.java
index 12d5bce..468d819 100644
--- a/biometric/src/main/java/androidx/biometric/FingerprintHelperFragment.java
+++ b/biometric/src/main/java/androidx/biometric/FingerprintHelperFragment.java
@@ -70,7 +70,7 @@
private void dismissAndForwardResult(final int errMsgId,
final CharSequence errString) {
- mHandler.obtainMessage(FingerprintDialogFragment.MSG_DISMISS_DIALOG)
+ mHandler.obtainMessage(FingerprintDialogFragment.MSG_DISMISS_DIALOG_ERROR)
.sendToTarget();
mExecutor.execute(new Runnable() {
@Override
@@ -124,7 +124,8 @@
public void onAuthenticationSucceeded(
final FingerprintManagerCompat.AuthenticationResult result) {
mHandler.obtainMessage(
- FingerprintDialogFragment.MSG_DISMISS_DIALOG).sendToTarget();
+ FingerprintDialogFragment.MSG_DISMISS_DIALOG_AUTHENTICATED)
+ .sendToTarget();
mExecutor.execute(new Runnable() {
@Override
public void run() {
@@ -182,7 +183,8 @@
FingerprintManagerCompat fingerprintManagerCompat = FingerprintManagerCompat.from(
mContext);
if (handlePreAuthenticationErrors(fingerprintManagerCompat)) {
- mHandler.obtainMessage(FingerprintDialogFragment.MSG_DISMISS_DIALOG).sendToTarget();
+ mHandler.obtainMessage(
+ FingerprintDialogFragment.MSG_DISMISS_DIALOG_ERROR).sendToTarget();
cleanup();
} else {
fingerprintManagerCompat.authenticate(
diff --git a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
index 4c8706c..8c6efff 100644
--- a/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/DiffAndDocs.kt
@@ -567,6 +567,7 @@
): TaskProvider<GenerateDocsTask> =
project.tasks.register(taskName, GenerateDocsTask::class.java) {
it.apply {
+ exclude("**/R.java")
dependsOn(generateSdkApiTask, doclavaConfig)
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Generates Java documentation in the style of d.android.com. To generate offline " +
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index bc192d8..1dbcbd3 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -68,7 +68,7 @@
val MEDIA2_EXOPLAYER = Version("1.0.0-alpha02")
val MEDIA2_WIDGET = Version("1.0.0-alpha07")
val MEDIAROUTER = Version("1.1.0-alpha03")
- val NAVIGATION = Version("2.1.0-alpha01")
+ val NAVIGATION = Version("2.1.0-alpha02")
val NAVIGATION_TESTING = Version("1.0.0-alpha08") // Unpublished
val PAGING = Version("2.2.0-alpha01")
val PALETTE = Version("1.1.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
index ac7f567..b448b23 100644
--- a/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/PublishDocsRules.kt
@@ -70,7 +70,7 @@
prebuilts(LibraryGroups.LIFECYCLE, "2.1.0-alpha03")
prebuilts(LibraryGroups.LOADER, "1.1.0-beta01")
prebuilts(LibraryGroups.LOCALBROADCASTMANAGER, "1.1.0-alpha01")
- prebuilts(LibraryGroups.MEDIA, "media", "1.1.0-alpha02")
+ prebuilts(LibraryGroups.MEDIA, "media", "1.1.0-alpha03")
// TODO: Rename media-widget to media2-widget after 1.0.0-alpha06
prebuilts(LibraryGroups.MEDIA, "media-widget", "1.0.0-alpha06")
ignore(LibraryGroups.MEDIA2.group, "media2-widget")
@@ -90,7 +90,8 @@
prebuilts(LibraryGroups.RECYCLERVIEW, "recyclerview", "1.1.0-alpha03")
prebuilts(LibraryGroups.RECYCLERVIEW, "recyclerview-selection", "1.1.0-alpha01")
prebuilts(LibraryGroups.REMOTECALLBACK, "1.0.0-alpha01")
- prebuilts(LibraryGroups.ROOM, "2.1.0-alpha05")
+ ignore(LibraryGroups.ROOM.group, "room-common-java8")
+ prebuilts(LibraryGroups.ROOM, "2.1.0-alpha06")
prebuilts(LibraryGroups.SAVEDSTATE, "1.0.0-alpha02")
prebuilts(LibraryGroups.SHARETARGET, "1.0.0-alpha01")
prebuilts(LibraryGroups.SLICE, "slice-builders", "1.0.0")
@@ -113,7 +114,7 @@
prebuilts(LibraryGroups.WEAR, "1.0.0")
.addStubs("wear/wear_stubs/com.google.android.wearable-stubs.jar")
prebuilts(LibraryGroups.WEBKIT, "1.0.0")
- prebuilts(LibraryGroups.WORKMANAGER, "2.0.0-rc01")
+ prebuilts(LibraryGroups.WORKMANAGER, "2.0.0")
default(Ignore)
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index e21a1e6..6d4dac2 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -87,9 +87,9 @@
"androidx.lifecycle:lifecycle-livedata:2.0.0"
const val ARCH_LIFECYCLE_VIEWMODEL = "androidx.lifecycle:lifecycle-viewmodel:2.0.0"
const val ARCH_LIFECYCLE_EXTENSIONS = "androidx.lifecycle:lifecycle-extensions:2.0.0"
-const val ARCH_CORE_COMMON = "androidx.arch.core:core-common:2.0.0@jar"
-const val ARCH_CORE_RUNTIME = "androidx.arch.core:core-runtime:2.0.0"
-const val ARCH_CORE_TESTING = "androidx.arch.core:core-testing:2.0.0"
+const val ARCH_CORE_COMMON = "androidx.arch.core:core-common:2.0.1@jar"
+const val ARCH_CORE_RUNTIME = "androidx.arch.core:core-runtime:2.0.1"
+const val ARCH_CORE_TESTING = "androidx.arch.core:core-testing:2.0.1"
const val SAFE_ARGS_ANDROID_GRADLE_PLUGIN = "com.android.tools.build:gradle:3.3.0"
const val SAFE_ARGS_KOTLIN_GRADLE_PLUGIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.20"
@@ -102,4 +102,7 @@
const val ARCH_ROOM_RXJAVA = "androidx.room:room-rxjava2:2.0.0"
const val ARCH_ROOM_TESTING = "androidx.room:room-testing:2.0.0"
+const val WORK_ARCH_CORE_RUNTIME = "androidx.arch.core:core-runtime:2.0.0"
+const val WORK_ARCH_CORE_TESTING = "androidx.arch.core:core-testing:2.0.0"
+
const val ROBOLECTRIC = "org.robolectric:robolectric:4.1"
diff --git a/car/core/api/1.0.0-alpha7.txt b/car/core/api/1.0.0-alpha7.txt
index a5d0f09..c3570ec 100644
--- a/car/core/api/1.0.0-alpha7.txt
+++ b/car/core/api/1.0.0-alpha7.txt
@@ -366,6 +366,7 @@
ctor public PagedListView(android.content.Context!, android.util.AttributeSet!, int, int);
method public void addItemDecoration(androidx.recyclerview.widget.RecyclerView.ItemDecoration);
method public void addOnItemTouchListener(androidx.recyclerview.widget.RecyclerView.OnItemTouchListener);
+ method public void addOnScrollListener(androidx.recyclerview.widget.RecyclerView.OnScrollListener);
method public androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>? getAdapter();
method public int getListContentBottomOffset();
method public int getListContentTopOffset();
@@ -384,8 +385,10 @@
method public void pageDown();
method public void pageUp();
method public int positionOf(android.view.View?);
+ method public void registerCallback(androidx.car.widget.PagedListView.Callback);
method public void removeItemDecoration(androidx.recyclerview.widget.RecyclerView.ItemDecoration);
method public void removeOnItemTouchListener(androidx.recyclerview.widget.RecyclerView.OnItemTouchListener);
+ method public void removeOnScrollListener(androidx.recyclerview.widget.RecyclerView.OnScrollListener);
method public void scrollToPosition(int);
method public void setAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>);
method public void setDividerColor(@ColorRes int);
@@ -397,16 +400,23 @@
method public void setListContentBottomOffset(@Px int);
method public void setListContentTopOffset(@Px int);
method public void setMaxPages(int);
- method public void setOnScrollListener(androidx.car.widget.PagedListView.OnScrollListener!);
+ method @Deprecated public void setOnScrollListener(androidx.car.widget.PagedListView.OnScrollListener!);
method public void setScrollBarContainerWidth(int);
method public void setScrollBarTopMargin(int);
method public void setScrollbarThumbEnabled(boolean);
method public void setUpButtonIcon(android.graphics.drawable.Drawable!);
method public void showAlphaJump();
method public void snapToPosition(int);
+ method public void unregisterCallback(androidx.car.widget.PagedListView.Callback);
field public static final int UNLIMITED_PAGES = -1; // 0xffffffff
}
+ public static interface PagedListView.Callback {
+ method public default void onReachBottom();
+ method public default void onScrollDownButtonClicked();
+ method public default void onScrollUpButtonClicked();
+ }
+
public static interface PagedListView.DividerVisibilityManager {
method public boolean getShowDivider(int);
}
@@ -427,13 +437,13 @@
method public void setPositionOffset(int);
}
- public abstract static class PagedListView.OnScrollListener {
- ctor public PagedListView.OnScrollListener();
- method public void onReachBottom();
- method public void onScrollDownButtonClicked();
- method public void onScrollStateChanged(androidx.recyclerview.widget.RecyclerView!, int);
- method public void onScrollUpButtonClicked();
- method public void onScrolled(androidx.recyclerview.widget.RecyclerView!, int, int);
+ @Deprecated public abstract static class PagedListView.OnScrollListener {
+ ctor @Deprecated public PagedListView.OnScrollListener();
+ method @Deprecated public void onReachBottom();
+ method @Deprecated public void onScrollDownButtonClicked();
+ method @Deprecated public void onScrollStateChanged(androidx.recyclerview.widget.RecyclerView!, int);
+ method @Deprecated public void onScrollUpButtonClicked();
+ method @Deprecated public void onScrolled(androidx.recyclerview.widget.RecyclerView!, int, int);
}
public class PagedScrollBarView extends android.view.ViewGroup {
@@ -567,6 +577,7 @@
method public void setEnabled(boolean);
method public void setPrimaryActionEmptyIcon();
method public void setPrimaryActionIcon(android.graphics.drawable.Icon, int);
+ method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
method public void setPrimaryActionNoIcon();
method public void setShowSwitchDivider(boolean);
method public void setSwitchOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener?);
diff --git a/car/core/api/current.txt b/car/core/api/current.txt
index a5d0f09..c3570ec 100644
--- a/car/core/api/current.txt
+++ b/car/core/api/current.txt
@@ -366,6 +366,7 @@
ctor public PagedListView(android.content.Context!, android.util.AttributeSet!, int, int);
method public void addItemDecoration(androidx.recyclerview.widget.RecyclerView.ItemDecoration);
method public void addOnItemTouchListener(androidx.recyclerview.widget.RecyclerView.OnItemTouchListener);
+ method public void addOnScrollListener(androidx.recyclerview.widget.RecyclerView.OnScrollListener);
method public androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>? getAdapter();
method public int getListContentBottomOffset();
method public int getListContentTopOffset();
@@ -384,8 +385,10 @@
method public void pageDown();
method public void pageUp();
method public int positionOf(android.view.View?);
+ method public void registerCallback(androidx.car.widget.PagedListView.Callback);
method public void removeItemDecoration(androidx.recyclerview.widget.RecyclerView.ItemDecoration);
method public void removeOnItemTouchListener(androidx.recyclerview.widget.RecyclerView.OnItemTouchListener);
+ method public void removeOnScrollListener(androidx.recyclerview.widget.RecyclerView.OnScrollListener);
method public void scrollToPosition(int);
method public void setAdapter(androidx.recyclerview.widget.RecyclerView.Adapter<? extends androidx.recyclerview.widget.RecyclerView.ViewHolder>);
method public void setDividerColor(@ColorRes int);
@@ -397,16 +400,23 @@
method public void setListContentBottomOffset(@Px int);
method public void setListContentTopOffset(@Px int);
method public void setMaxPages(int);
- method public void setOnScrollListener(androidx.car.widget.PagedListView.OnScrollListener!);
+ method @Deprecated public void setOnScrollListener(androidx.car.widget.PagedListView.OnScrollListener!);
method public void setScrollBarContainerWidth(int);
method public void setScrollBarTopMargin(int);
method public void setScrollbarThumbEnabled(boolean);
method public void setUpButtonIcon(android.graphics.drawable.Drawable!);
method public void showAlphaJump();
method public void snapToPosition(int);
+ method public void unregisterCallback(androidx.car.widget.PagedListView.Callback);
field public static final int UNLIMITED_PAGES = -1; // 0xffffffff
}
+ public static interface PagedListView.Callback {
+ method public default void onReachBottom();
+ method public default void onScrollDownButtonClicked();
+ method public default void onScrollUpButtonClicked();
+ }
+
public static interface PagedListView.DividerVisibilityManager {
method public boolean getShowDivider(int);
}
@@ -427,13 +437,13 @@
method public void setPositionOffset(int);
}
- public abstract static class PagedListView.OnScrollListener {
- ctor public PagedListView.OnScrollListener();
- method public void onReachBottom();
- method public void onScrollDownButtonClicked();
- method public void onScrollStateChanged(androidx.recyclerview.widget.RecyclerView!, int);
- method public void onScrollUpButtonClicked();
- method public void onScrolled(androidx.recyclerview.widget.RecyclerView!, int, int);
+ @Deprecated public abstract static class PagedListView.OnScrollListener {
+ ctor @Deprecated public PagedListView.OnScrollListener();
+ method @Deprecated public void onReachBottom();
+ method @Deprecated public void onScrollDownButtonClicked();
+ method @Deprecated public void onScrollStateChanged(androidx.recyclerview.widget.RecyclerView!, int);
+ method @Deprecated public void onScrollUpButtonClicked();
+ method @Deprecated public void onScrolled(androidx.recyclerview.widget.RecyclerView!, int, int);
}
public class PagedScrollBarView extends android.view.ViewGroup {
@@ -567,6 +577,7 @@
method public void setEnabled(boolean);
method public void setPrimaryActionEmptyIcon();
method public void setPrimaryActionIcon(android.graphics.drawable.Icon, int);
+ method public void setPrimaryActionIcon(android.graphics.drawable.Drawable, int);
method public void setPrimaryActionNoIcon();
method public void setShowSwitchDivider(boolean);
method public void setSwitchOnCheckedChangeListener(android.widget.CompoundButton.OnCheckedChangeListener?);
diff --git a/car/core/api/restricted_1.0.0-alpha7.txt b/car/core/api/restricted_1.0.0-alpha7.txt
index 8a43c31..7dc7128 100644
--- a/car/core/api/restricted_1.0.0-alpha7.txt
+++ b/car/core/api/restricted_1.0.0-alpha7.txt
@@ -44,7 +44,7 @@
method public static void apply(android.content.Context!, androidx.car.uxrestrictions.CarUxRestrictions!, android.widget.TextView!);
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DropShadowScrollListener extends androidx.car.widget.PagedListView.OnScrollListener {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DropShadowScrollListener extends androidx.recyclerview.widget.RecyclerView.OnScrollListener {
ctor public DropShadowScrollListener(android.view.View!);
}
diff --git a/car/core/api/restricted_current.txt b/car/core/api/restricted_current.txt
index 8a43c31..7dc7128 100644
--- a/car/core/api/restricted_current.txt
+++ b/car/core/api/restricted_current.txt
@@ -44,7 +44,7 @@
method public static void apply(android.content.Context!, androidx.car.uxrestrictions.CarUxRestrictions!, android.widget.TextView!);
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DropShadowScrollListener extends androidx.car.widget.PagedListView.OnScrollListener {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class DropShadowScrollListener extends androidx.recyclerview.widget.RecyclerView.OnScrollListener {
ctor public DropShadowScrollListener(android.view.View!);
}
diff --git a/car/core/res/values/styles.xml b/car/core/res/values/styles.xml
index c3eb2cd..517cc0c 100644
--- a/car/core/res/values/styles.xml
+++ b/car/core/res/values/styles.xml
@@ -72,7 +72,7 @@
</style>
<style name="TextAppearance.Car.Body1.Medium.Dark">
- <item name="android:textColor">@color/car_body1_light</item>
+ <item name="android:textColor">@color/car_body1_dark</item>
</style>
<!-- An alternate styling for body text that is both a different color and size than
diff --git a/car/core/src/androidTest/java/androidx/car/widget/PagedListViewTest.java b/car/core/src/androidTest/java/androidx/car/widget/PagedListViewTest.java
index ef41766..32b9931 100644
--- a/car/core/src/androidTest/java/androidx/car/widget/PagedListViewTest.java
+++ b/car/core/src/androidTest/java/androidx/car/widget/PagedListViewTest.java
@@ -36,6 +36,9 @@
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import android.content.pm.PackageManager;
import android.content.res.Resources;
@@ -179,6 +182,76 @@
}
@Test
+ public void testScrollButtonCallback() {
+ int itemCount = ITEMS_PER_PAGE * 3;
+ setUpPagedListView(itemCount);
+
+ PagedListView.Callback mockedCallbackOne = mock(PagedListView.Callback.class);
+ PagedListView.Callback mockedCallbackTwo = mock(PagedListView.Callback.class);
+ PagedListView.Callback mockedCallbackThree = mock(PagedListView.Callback.class);
+
+ mPagedListView.registerCallback(mockedCallbackOne);
+ mPagedListView.registerCallback(mockedCallbackTwo);
+ mPagedListView.registerCallback(mockedCallbackThree);
+
+ // Move one page down.
+ onView(withId(R.id.page_down)).perform(click());
+ verify(mockedCallbackOne, times(1)).onScrollDownButtonClicked();
+ verify(mockedCallbackTwo, times(1)).onScrollDownButtonClicked();
+ verify(mockedCallbackThree, times(1)).onScrollDownButtonClicked();
+
+ // Move one page up.
+ onView(withId(R.id.page_up)).perform(click());
+ verify(mockedCallbackOne, times(1)).onScrollUpButtonClicked();
+ verify(mockedCallbackTwo, times(1)).onScrollUpButtonClicked();
+ verify(mockedCallbackThree, times(1)).onScrollUpButtonClicked();
+
+ mPagedListView.unregisterCallback(mockedCallbackOne);
+ onView(withId(R.id.page_down)).perform(click());
+ verify(mockedCallbackOne, times(1)).onScrollDownButtonClicked();
+ verify(mockedCallbackTwo, times(2)).onScrollDownButtonClicked();
+ verify(mockedCallbackThree, times(2)).onScrollDownButtonClicked();
+ }
+
+ @Test
+ public void testMultipleScrollButtonCallback() {
+ int itemCount = ITEMS_PER_PAGE * 4;
+ setUpPagedListView(itemCount);
+
+ PagedListView.Callback mockedCallback = mock(PagedListView.Callback.class);
+ mPagedListView.registerCallback(mockedCallback);
+
+ // Move one page down.
+ onView(withId(R.id.page_down)).perform(click());
+ onView(withId(R.id.page_down)).perform(click());
+ onView(withId(R.id.page_down)).perform(click());
+ verify(mockedCallback, times(3)).onScrollDownButtonClicked();
+ }
+
+ @Test
+ public void testReachBottomCallback() {
+ int itemCount = ITEMS_PER_PAGE * 2;
+ setUpPagedListView(itemCount);
+
+ PagedListView.Callback mockedCallback = mock(PagedListView.Callback.class);
+ mPagedListView.registerCallback(mockedCallback);
+
+ // Moving down to bottom of list.
+ onView(withId(R.id.page_down)).perform(click());
+ onView(withId(R.id.page_down)).perform(click());
+
+ verify(mockedCallback, times(1)).onReachBottom();
+
+ // Moving up should not cause a onReachBottom event.
+ onView(withId(R.id.page_up)).perform(click());
+ verify(mockedCallback, times(1)).onReachBottom();
+
+ // Move to bottom of list again.
+ onView(withId(R.id.page_down)).perform(click());
+ verify(mockedCallback, times(2)).onReachBottom();
+ }
+
+ @Test
public void testPageUpButtonDisabledAtTop() {
int itemCount = ITEMS_PER_PAGE * 3;
setUpPagedListView(itemCount);
diff --git a/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java b/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
index cd842bd..ee19dda 100644
--- a/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
+++ b/car/core/src/androidTest/java/androidx/car/widget/SwitchListItemTest.java
@@ -474,7 +474,7 @@
}
@Test
- public void testSetPrimaryActionIcon() {
+ public void testSetPrimaryActionIcon_withIcon() {
SwitchListItem item = new SwitchListItem(mActivity);
item.setPrimaryActionIcon(
Icon.createWithResource(mActivity, android.R.drawable.sym_def_app_icon),
@@ -487,6 +487,19 @@
}
@Test
+ public void testSetPrimaryActionIcon_withDrawable() {
+ SwitchListItem item = new SwitchListItem(mActivity);
+ item.setPrimaryActionIcon(
+ mActivity.getDrawable(android.R.drawable.sym_def_app_icon),
+ SwitchListItem.PRIMARY_ACTION_ICON_SIZE_LARGE);
+
+ List<SwitchListItem> items = Arrays.asList(item);
+ setupPagedListView(items);
+
+ assertThat(getViewHolderAtPosition(0).getPrimaryIcon().getDrawable(), is(notNullValue()));
+ }
+
+ @Test
public void testPrimaryIconSizesInIncreasingOrder() {
SwitchListItem small = new SwitchListItem(mActivity);
small.setPrimaryActionIcon(
diff --git a/car/core/src/main/java/androidx/car/app/CarListDialog.java b/car/core/src/main/java/androidx/car/app/CarListDialog.java
index 47bf005..b49ef1b 100644
--- a/car/core/src/main/java/androidx/car/app/CarListDialog.java
+++ b/car/core/src/main/java/androidx/car/app/CarListDialog.java
@@ -164,7 +164,7 @@
return;
}
- mList.setOnScrollListener(new DropShadowScrollListener(mTitleView));
+ mList.addOnScrollListener(new DropShadowScrollListener(mTitleView));
}
@Override
diff --git a/car/core/src/main/java/androidx/car/drawer/CarDrawerController.java b/car/core/src/main/java/androidx/car/drawer/CarDrawerController.java
index afcec38..74743c2 100644
--- a/car/core/src/main/java/androidx/car/drawer/CarDrawerController.java
+++ b/car/core/src/main/java/androidx/car/drawer/CarDrawerController.java
@@ -117,7 +117,7 @@
theme.resolveAttribute(R.attr.drawerToolbarId, outValue, true)
? outValue.resourceId
: R.id.drawer_toolbar);
- mDrawerList.setOnScrollListener(new DropShadowScrollListener(toolbar));
+ mDrawerList.addOnScrollListener(new DropShadowScrollListener(toolbar));
@IdRes int backButtonId = theme.resolveAttribute(R.attr.drawerBackButtonId, outValue, true)
? outValue.resourceId
diff --git a/car/core/src/main/java/androidx/car/util/DropShadowScrollListener.java b/car/core/src/main/java/androidx/car/util/DropShadowScrollListener.java
index 4d3a4fd..1c3567e 100644
--- a/car/core/src/main/java/androidx/car/util/DropShadowScrollListener.java
+++ b/car/core/src/main/java/androidx/car/util/DropShadowScrollListener.java
@@ -37,7 +37,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class DropShadowScrollListener extends PagedListView.OnScrollListener {
+public class DropShadowScrollListener extends RecyclerView.OnScrollListener {
private static final String TAG = "DropShadowScrollListener";
private static final int ANIMATION_DURATION_MS = 100;
diff --git a/car/core/src/main/java/androidx/car/widget/PagedListView.java b/car/core/src/main/java/androidx/car/widget/PagedListView.java
index f5c1a70..8ad0ec7 100644
--- a/car/core/src/main/java/androidx/car/widget/PagedListView.java
+++ b/car/core/src/main/java/androidx/car/widget/PagedListView.java
@@ -51,6 +51,8 @@
import androidx.recyclerview.widget.RecyclerView;
import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
/**
* View that wraps a {@link RecyclerView} and a scroll bar that has
@@ -106,7 +108,8 @@
* which point we'll construct it and add it to the view hierarchy as a child of this frame
* layout.
*/
- @Nullable private AlphaJumpOverlayView mAlphaJumpView;
+ @Nullable
+ private AlphaJumpOverlayView mAlphaJumpView;
private int mRowsPerPage = -1;
private RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter;
@@ -114,6 +117,8 @@
/** Maximum number of pages to show. */
private int mMaxPages = UNLIMITED_PAGES;
+ /** Package private to allow access to nested classes. */
+ final List<Callback> mCallbacks = new ArrayList<>();
OnScrollListener mOnScrollListener;
/** Used to check if there are more items added to the list. */
@@ -244,7 +249,34 @@
mSnapHelper = new PagedSnapHelper(context);
mSnapHelper.attachToRecyclerView(mRecyclerView);
- mRecyclerView.addOnScrollListener(mRecyclerViewOnScrollListener);
+ mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChanged(recyclerView, newState);
+ }
+ if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS);
+ }
+ }
+
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrolled(recyclerView, dx, dy);
+
+ if (!isAtStart() && isAtEnd()) {
+ mOnScrollListener.onReachBottom();
+ }
+ }
+ if (!isAtStart() && isAtEnd()) {
+ for (Callback callback : mCallbacks) {
+ callback.onReachBottom();
+ }
+ }
+ updatePaginationButtons(false);
+ }
+ });
mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);
if (a.getBoolean(R.styleable.PagedListView_verticallyCenterListContent, false)) {
@@ -298,12 +330,18 @@
switch (direction) {
case PagedScrollBarView.PaginationListener.PAGE_UP:
pageUp();
+ for (Callback callback : mCallbacks) {
+ callback.onScrollUpButtonClicked();
+ }
if (mOnScrollListener != null) {
mOnScrollListener.onScrollUpButtonClicked();
}
break;
case PagedScrollBarView.PaginationListener.PAGE_DOWN:
pageDown();
+ for (Callback callback : mCallbacks) {
+ callback.onScrollDownButtonClicked();
+ }
if (mOnScrollListener != null) {
mOnScrollListener.onScrollDownButtonClicked();
}
@@ -831,11 +869,53 @@
* PagedListView.
*
* @param listener The scroll listener to set.
+ * @deprecated Use {@link #addOnScrollListener(RecyclerView.OnScrollListener)} to be notified
+ * of scroll events within the PagedListView. To be notified of other PagedListView events, use
+ * {@link #registerCallback(Callback)}.
*/
+ @Deprecated
public void setOnScrollListener(OnScrollListener listener) {
mOnScrollListener = listener;
}
+ /**
+ * Adds a {@link RecyclerView.OnScrollListener} that will be notified of scroll events
+ * within the PagedListView.
+ *
+ * @param listener The scroll listener to add.
+ */
+ public void addOnScrollListener(@NonNull RecyclerView.OnScrollListener listener) {
+ mRecyclerView.addOnScrollListener(listener);
+ }
+
+ /**
+ * Remove a {@link RecyclerView.OnScrollListener} that was notified of scroll events
+ * within the PagedListView.
+ *
+ * @param listener The scroll listener to remove.
+ */
+ public void removeOnScrollListener(@NonNull RecyclerView.OnScrollListener listener) {
+ mRecyclerView.removeOnScrollListener(listener);
+ }
+
+ /**
+ * Add a {@link Callback} that will be notified of PagedListView events.
+ *
+ * @param callback The callback to add.
+ */
+ public void registerCallback(@NonNull Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ /**
+ * Remove a {@link Callback} that was notified of PagedListView events.
+ *
+ * @param callback The callback to remove.
+ */
+ public void unregisterCallback(@NonNull Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
/** Returns the page the given position is on, starting with page 0. */
public int getPage(int position) {
if (mRowsPerPage == -1) {
@@ -1246,31 +1326,6 @@
}
}
- private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
- new RecyclerView.OnScrollListener() {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- if (mOnScrollListener != null) {
- mOnScrollListener.onScrolled(recyclerView, dx, dy);
-
- if (!isAtStart() && isAtEnd()) {
- mOnScrollListener.onReachBottom();
- }
- }
- updatePaginationButtons(false);
- }
-
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- if (mOnScrollListener != null) {
- mOnScrollListener.onScrollStateChanged(recyclerView, newState);
- }
- if (newState == RecyclerView.SCROLL_STATE_IDLE) {
- mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS);
- }
- }
- };
-
final Runnable mPaginationRunnable =
new Runnable() {
@Override
@@ -1291,25 +1346,56 @@
private final Runnable mUpdatePaginationRunnable =
() -> updatePaginationButtons(true /*animate*/);
- /** Used to listen for {@code PagedListView} scroll events. */
+ /** Used to listen for {@code PagedListView} events. */
+ public interface Callback {
+ /**
+ * Called when the {@code PagedListView} has been scrolled so that the last item is
+ * completely visible.
+ */
+ default void onReachBottom() {
+ }
+
+ /** Called when scroll up button is clicked */
+ default void onScrollUpButtonClicked() {
+ }
+
+ /** Called when scroll down button is clicked */
+ default void onScrollDownButtonClicked() {
+ }
+ }
+
+ /**
+ * Used to listen for {@code PagedListView} scroll events.
+ *
+ * @deprecated Use {@link RecyclerView.OnScrollListener} to be notified of scroll events within
+ * the PagedListView. To be notified of other PagedListView events, use {@link Callback}.
+ */
+ @Deprecated
public abstract static class OnScrollListener {
/**
* Called when the {@code PagedListView} has been scrolled so that the last item is
* completely visible.
*/
- public void onReachBottom() {}
+ public void onReachBottom() {
+ }
+
/** Called when scroll up button is clicked */
- public void onScrollUpButtonClicked() {}
+ public void onScrollUpButtonClicked() {
+ }
+
/** Called when scroll down button is clicked */
- public void onScrollDownButtonClicked() {}
+ public void onScrollDownButtonClicked() {
+ }
/**
* Called when RecyclerView.OnScrollListener#onScrolled is called. See
* RecyclerView.OnScrollListener
*/
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {}
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ }
/** See RecyclerView.OnScrollListener */
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {}
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ }
}
}
diff --git a/car/core/src/main/java/androidx/car/widget/SwitchListItem.java b/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
index d0ae858..d34820d 100644
--- a/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
+++ b/car/core/src/main/java/androidx/car/widget/SwitchListItem.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Handler;
import android.os.Looper;
@@ -114,6 +115,7 @@
@PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
private Icon mPrimaryActionIcon;
+ private Drawable mPrimaryActionIconDrawable;
@PrimaryActionIconSize private int mPrimaryActionIconSize = PRIMARY_ACTION_ICON_SIZE_SMALL;
private CharSequence mTitle;
@@ -202,6 +204,9 @@
/**
* Sets {@code Primary Action} to be represented by an icon.
*
+ * <p>If both this method and {@link #setPrimaryActionIcon(Drawable,int)} are called, then
+ * this method will take precedence.
+ *
* @param icon An icon to set as primary action.
* @param size small/medium/large. Available as {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
* {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM},
@@ -215,6 +220,24 @@
}
/**
+ * Sets {@code Primary Action} to be represented by an icon.
+ *
+ * <p>If both this method and {@link #setPrimaryActionIcon(Icon,int)} are called, then
+ * the other method will take precedence.
+ *
+ * @param drawable the Drawable to set.
+ * @param size small/medium/large. Available as {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
+ * {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM},
+ * {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
+ */
+ public void setPrimaryActionIcon(@NonNull Drawable drawable, @PrimaryActionIconSize int size) {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_ICON;
+ mPrimaryActionIconDrawable = drawable;
+ mPrimaryActionIconSize = size;
+ markDirty();
+ }
+
+ /**
* Sets {@code Primary Action} to be empty icon.
*
* <p>{@code Text} would have a start margin as if {@code Primary Action} were set to primary
@@ -324,9 +347,13 @@
case PRIMARY_ACTION_TYPE_ICON:
mBinders.add(vh -> {
vh.getPrimaryIcon().setVisibility(View.VISIBLE);
- mPrimaryActionIcon.loadDrawableAsync(getContext(),
- drawable -> vh.getPrimaryIcon().setImageDrawable(drawable),
- new Handler(Looper.getMainLooper()));
+ if (mPrimaryActionIcon != null) {
+ mPrimaryActionIcon.loadDrawableAsync(getContext(),
+ drawable -> vh.getPrimaryIcon().setImageDrawable(drawable),
+ new Handler(Looper.getMainLooper()));
+ } else {
+ vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
+ }
});
break;
case PRIMARY_ACTION_TYPE_EMPTY_ICON:
diff --git a/core/api/1.1.0-alpha06.txt b/core/api/1.1.0-alpha06.txt
index 0ece2e3..84939d0 100644
--- a/core/api/1.1.0-alpha06.txt
+++ b/core/api/1.1.0-alpha06.txt
@@ -633,10 +633,9 @@
method public androidx.core.app.Person.Builder setUri(String?);
}
- public final class RemoteActionCompat {
+ public final class RemoteActionCompat implements androidx.versionedparcelable.VersionedParcelable {
ctor public RemoteActionCompat(androidx.core.graphics.drawable.IconCompat, CharSequence, CharSequence, android.app.PendingIntent);
ctor public RemoteActionCompat(androidx.core.app.RemoteActionCompat);
- method public static androidx.core.app.RemoteActionCompat createFromBundle(android.os.Bundle);
method @RequiresApi(26) public static androidx.core.app.RemoteActionCompat createFromRemoteAction(android.app.RemoteAction);
method public android.app.PendingIntent getActionIntent();
method public CharSequence getContentDescription();
@@ -646,7 +645,6 @@
method public void setEnabled(boolean);
method public void setShouldShowIcon(boolean);
method public boolean shouldShowIcon();
- method public android.os.Bundle toBundle();
method @RequiresApi(26) public android.app.RemoteAction toRemoteAction();
}
diff --git a/core/api/current.txt b/core/api/current.txt
index 0ece2e3..84939d0 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -633,10 +633,9 @@
method public androidx.core.app.Person.Builder setUri(String?);
}
- public final class RemoteActionCompat {
+ public final class RemoteActionCompat implements androidx.versionedparcelable.VersionedParcelable {
ctor public RemoteActionCompat(androidx.core.graphics.drawable.IconCompat, CharSequence, CharSequence, android.app.PendingIntent);
ctor public RemoteActionCompat(androidx.core.app.RemoteActionCompat);
- method public static androidx.core.app.RemoteActionCompat createFromBundle(android.os.Bundle);
method @RequiresApi(26) public static androidx.core.app.RemoteActionCompat createFromRemoteAction(android.app.RemoteAction);
method public android.app.PendingIntent getActionIntent();
method public CharSequence getContentDescription();
@@ -646,7 +645,6 @@
method public void setEnabled(boolean);
method public void setShouldShowIcon(boolean);
method public boolean shouldShowIcon();
- method public android.os.Bundle toBundle();
method @RequiresApi(26) public android.app.RemoteAction toRemoteAction();
}
diff --git a/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java b/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
index 47da629..a299a46 100644
--- a/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
+++ b/core/src/androidTest/java/androidx/core/app/RemoteActionCompatTest.java
@@ -21,65 +21,65 @@
import android.app.PendingIntent;
import android.content.Intent;
+import android.os.Parcel;
import android.support.v4.BaseInstrumentationTestCase;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.core.app.ApplicationProvider;
+import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import androidx.versionedparcelable.ParcelUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RemoteActionCompatTest extends BaseInstrumentationTestCase<TestActivity> {
+ private static final IconCompat ICON = IconCompat.createWithContentUri("content://test");
+ private static final String TITLE = "title";
+ private static final String DESCRIPTION = "description";
+ private static final PendingIntent ACTION = PendingIntent.getBroadcast(
+ InstrumentationRegistry.getContext(), 0, new Intent("TESTACTION"), 0);
public RemoteActionCompatTest() {
super(TestActivity.class);
}
@Test
- public void testRemoteAction_bundle() throws Throwable {
- IconCompat icon = IconCompat.createWithContentUri("content://test");
- String title = "title";
- String description = "description";
- PendingIntent action = PendingIntent.getBroadcast(
- ApplicationProvider.getApplicationContext(), 0,
- new Intent("TESTACTION"), 0);
- RemoteActionCompat reference = new RemoteActionCompat(icon, title, description, action);
- reference.setEnabled(false);
- reference.setShouldShowIcon(false);
-
- RemoteActionCompat result = RemoteActionCompat.createFromBundle(reference.toBundle());
-
- assertEquals(icon.getUri(), result.getIcon().getUri());
- assertEquals(title, result.getTitle());
- assertEquals(description, result.getContentDescription());
- assertEquals(action.getTargetPackage(), result.getActionIntent().getTargetPackage());
- assertFalse(result.isEnabled());
- assertFalse(result.shouldShowIcon());
+ public void testRemoteAction_shallowCopy() throws Throwable {
+ RemoteActionCompat reference = createTestRemoteActionCompat();
+ RemoteActionCompat result = new RemoteActionCompat(reference);
+ assertEqualsToTestRemoteActionCompat(result);
}
@Test
- public void testRemoteAction_shallowCopy() throws Throwable {
- IconCompat icon = IconCompat.createWithContentUri("content://test");
- String title = "title";
- String description = "description";
- PendingIntent action = PendingIntent.getBroadcast(
- ApplicationProvider.getApplicationContext(), 0,
- new Intent("TESTACTION"), 0);
- RemoteActionCompat reference = new RemoteActionCompat(icon, title, description, action);
+ public void testRemoteAction_parcel() {
+ RemoteActionCompat reference = createTestRemoteActionCompat();
+
+ Parcel p = Parcel.obtain();
+ p.writeParcelable(ParcelUtils.toParcelable(reference), 0);
+ p.setDataPosition(0);
+ RemoteActionCompat result = ParcelUtils.fromParcelable(
+ p.readParcelable(getClass().getClassLoader()));
+
+ assertEqualsToTestRemoteActionCompat(result);
+ }
+
+ private RemoteActionCompat createTestRemoteActionCompat() {
+ RemoteActionCompat reference = new RemoteActionCompat(ICON, TITLE, DESCRIPTION, ACTION);
reference.setEnabled(false);
reference.setShouldShowIcon(false);
-
- RemoteActionCompat result = new RemoteActionCompat(reference);
-
- assertEquals(icon.getUri(), result.getIcon().getUri());
- assertEquals(title, result.getTitle());
- assertEquals(description, result.getContentDescription());
- assertEquals(action.getTargetPackage(), result.getActionIntent().getTargetPackage());
- assertFalse(result.isEnabled());
- assertFalse(result.shouldShowIcon());
+ return reference;
}
-}
+
+ private void assertEqualsToTestRemoteActionCompat(RemoteActionCompat remoteAction) {
+ assertEquals(ICON.getUri(), remoteAction.getIcon().getUri());
+ assertEquals(TITLE, remoteAction.getTitle());
+ assertEquals(DESCRIPTION, remoteAction.getContentDescription());
+ assertEquals(ACTION.getTargetPackage(), remoteAction.getActionIntent().getTargetPackage());
+ assertFalse(remoteAction.isEnabled());
+ assertFalse(remoteAction.shouldShowIcon());
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/androidx/core/app/RemoteActionCompat.java b/core/src/main/java/androidx/core/app/RemoteActionCompat.java
index faf676c..28f89c7 100644
--- a/core/src/main/java/androidx/core/app/RemoteActionCompat.java
+++ b/core/src/main/java/androidx/core/app/RemoteActionCompat.java
@@ -19,12 +19,14 @@
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.os.Build;
-import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.util.Preconditions;
+import androidx.versionedparcelable.ParcelField;
+import androidx.versionedparcelable.VersionedParcelable;
+import androidx.versionedparcelable.VersionedParcelize;
/**
* Represents a remote action that can be called from another process. The action can have an
@@ -32,21 +34,20 @@
* <p>
* This is a backward-compatible version of {@link RemoteAction}.
*/
-public final class RemoteActionCompat {
-
- private static final String EXTRA_ICON = "icon";
- private static final String EXTRA_TITLE = "title";
- private static final String EXTRA_CONTENT_DESCRIPTION = "desc";
- private static final String EXTRA_ACTION_INTENT = "action";
- private static final String EXTRA_ENABLED = "enabled";
- private static final String EXTRA_SHOULD_SHOW_ICON = "showicon";
-
- private final IconCompat mIcon;
- private final CharSequence mTitle;
- private final CharSequence mContentDescription;
- private final PendingIntent mActionIntent;
- private boolean mEnabled;
- private boolean mShouldShowIcon;
+@VersionedParcelize(jetifyAs = "android.support.v4.app.RemoteActionCompat")
+public final class RemoteActionCompat implements VersionedParcelable {
+ @ParcelField(1)
+ IconCompat mIcon;
+ @ParcelField(2)
+ CharSequence mTitle;
+ @ParcelField(3)
+ CharSequence mContentDescription;
+ @ParcelField(4)
+ PendingIntent mActionIntent;
+ @ParcelField(5)
+ boolean mEnabled;
+ @ParcelField(6)
+ boolean mShouldShowIcon;
public RemoteActionCompat(@NonNull IconCompat icon, @NonNull CharSequence title,
@NonNull CharSequence contentDescription, @NonNull PendingIntent intent) {
@@ -59,6 +60,11 @@
}
/**
+ * Used for VersionedParcelable.
+ */
+ RemoteActionCompat() {}
+
+ /**
* Constructs a {@link RemoteActionCompat} using data from {@code other}.
*/
public RemoteActionCompat(@NonNull RemoteActionCompat other) {
@@ -160,35 +166,4 @@
}
return action;
}
-
- /**
- * Converts this into a Bundle that can be converted back to a {@link RemoteActionCompat}
- * by calling {@link #createFromBundle(Bundle)}.
- */
- @NonNull
- public Bundle toBundle() {
- Bundle bundle = new Bundle();
- bundle.putBundle(EXTRA_ICON, mIcon.toBundle());
- bundle.putCharSequence(EXTRA_TITLE, mTitle);
- bundle.putCharSequence(EXTRA_CONTENT_DESCRIPTION, mContentDescription);
- bundle.putParcelable(EXTRA_ACTION_INTENT, mActionIntent);
- bundle.putBoolean(EXTRA_ENABLED, mEnabled);
- bundle.putBoolean(EXTRA_SHOULD_SHOW_ICON, mShouldShowIcon);
- return bundle;
- }
-
- /**
- * Converts the bundle created by {@link #toBundle()} back to {@link RemoteActionCompat}.
- */
- @NonNull
- public static RemoteActionCompat createFromBundle(@NonNull Bundle bundle) {
- RemoteActionCompat action = new RemoteActionCompat(
- IconCompat.createFromBundle(bundle.getBundle(EXTRA_ICON)),
- bundle.getCharSequence(EXTRA_TITLE),
- bundle.getCharSequence(EXTRA_CONTENT_DESCRIPTION),
- bundle.<PendingIntent>getParcelable(EXTRA_ACTION_INTENT));
- action.setEnabled(bundle.getBoolean(EXTRA_ENABLED));
- action.setShouldShowIcon(bundle.getBoolean(EXTRA_SHOULD_SHOW_ICON));
- return action;
- }
}
diff --git a/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java b/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
index 1e6ca0e..8bf3e30 100644
--- a/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
+++ b/core/src/main/java/androidx/core/graphics/TypefaceCompatUtil.java
@@ -60,9 +60,14 @@
*/
@Nullable
public static File getTempFile(Context context) {
+ File cacheDir = context.getCacheDir();
+ if (cacheDir == null) {
+ return null;
+ }
+
final String prefix = CACHE_FILE_PREFIX + Process.myPid() + "-" + Process.myTid() + "-";
for (int i = 0; i < 100; ++i) {
- final File file = new File(context.getCacheDir(), prefix + i);
+ final File file = new File(cacheDir, prefix + i);
try {
if (file.createNewFile()) {
return file;
diff --git a/fragment/api/1.1.0-alpha06.txt b/fragment/api/1.1.0-alpha06.txt
index e586a2f..1c78cb8 100644
--- a/fragment/api/1.1.0-alpha06.txt
+++ b/fragment/api/1.1.0-alpha06.txt
@@ -331,14 +331,13 @@
method public boolean isViewFromObject(android.view.View, Object);
}
- public class FragmentTabHost extends android.widget.TabHost implements android.widget.TabHost.OnTabChangeListener {
- ctor public FragmentTabHost(android.content.Context);
- ctor public FragmentTabHost(android.content.Context, android.util.AttributeSet?);
- method public void addTab(android.widget.TabHost.TabSpec, Class<?>, android.os.Bundle?);
- method public void onTabChanged(String?);
- method @Deprecated public void setup();
- method public void setup(android.content.Context, androidx.fragment.app.FragmentManager);
- method public void setup(android.content.Context, androidx.fragment.app.FragmentManager, int);
+ @Deprecated public class FragmentTabHost extends android.widget.TabHost implements android.widget.TabHost.OnTabChangeListener {
+ ctor @Deprecated public FragmentTabHost(android.content.Context);
+ ctor @Deprecated public FragmentTabHost(android.content.Context, android.util.AttributeSet?);
+ method @Deprecated public void addTab(android.widget.TabHost.TabSpec, Class<?>, android.os.Bundle?);
+ method @Deprecated public void onTabChanged(String?);
+ method @Deprecated public void setup(android.content.Context, androidx.fragment.app.FragmentManager);
+ method @Deprecated public void setup(android.content.Context, androidx.fragment.app.FragmentManager, int);
}
public abstract class FragmentTransaction {
diff --git a/fragment/api/current.txt b/fragment/api/current.txt
index e586a2f..1c78cb8 100644
--- a/fragment/api/current.txt
+++ b/fragment/api/current.txt
@@ -331,14 +331,13 @@
method public boolean isViewFromObject(android.view.View, Object);
}
- public class FragmentTabHost extends android.widget.TabHost implements android.widget.TabHost.OnTabChangeListener {
- ctor public FragmentTabHost(android.content.Context);
- ctor public FragmentTabHost(android.content.Context, android.util.AttributeSet?);
- method public void addTab(android.widget.TabHost.TabSpec, Class<?>, android.os.Bundle?);
- method public void onTabChanged(String?);
- method @Deprecated public void setup();
- method public void setup(android.content.Context, androidx.fragment.app.FragmentManager);
- method public void setup(android.content.Context, androidx.fragment.app.FragmentManager, int);
+ @Deprecated public class FragmentTabHost extends android.widget.TabHost implements android.widget.TabHost.OnTabChangeListener {
+ ctor @Deprecated public FragmentTabHost(android.content.Context);
+ ctor @Deprecated public FragmentTabHost(android.content.Context, android.util.AttributeSet?);
+ method @Deprecated public void addTab(android.widget.TabHost.TabSpec, Class<?>, android.os.Bundle?);
+ method @Deprecated public void onTabChanged(String?);
+ method @Deprecated public void setup(android.content.Context, androidx.fragment.app.FragmentManager);
+ method @Deprecated public void setup(android.content.Context, androidx.fragment.app.FragmentManager, int);
}
public abstract class FragmentTransaction {
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.java
deleted file mode 100644
index 900ec9d..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Counts the number of onCreateView, onHiddenChanged (onHide, onShow), onAttach, and onDetach
- * calls.
- */
-public class CountCallsFragment extends StrictViewFragment {
- public int onCreateViewCount = 0;
- public int onDestroyViewCount = 0;
- public int onHideCount = 0;
- public int onShowCount = 0;
- public int onAttachCount = 0;
- public int onDetachCount = 0;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- onCreateViewCount++;
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- @Override
- public void onHiddenChanged(boolean hidden) {
- if (hidden) {
- onHideCount++;
- } else {
- onShowCount++;
- }
- super.onHiddenChanged(hidden);
- }
-
- @Override
- public void onAttach(Context context) {
- onAttachCount++;
- super.onAttach(context);
- }
-
- @Override
- public void onDetach() {
- onDetachCount++;
- super.onDetach();
- }
-
- @Override
- public void onDestroyView() {
- onDestroyViewCount++;
- super.onDestroyView();
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.kt b/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.kt
new file mode 100644
index 0000000..e1e3378
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/CountCallsFragment.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 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 androidx.fragment.app
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.fragment.test.R
+
+/**
+ * Counts the number of onCreateView, onHiddenChanged (onHide, onShow), onAttach, and onDetach
+ * calls.
+ */
+class CountCallsFragment(
+ @LayoutRes contentLayoutId: Int = R.layout.strict_view_fragment
+) : StrictViewFragment(contentLayoutId) {
+ var onCreateViewCount = 0
+ var onDestroyViewCount = 0
+ var onHideCount = 0
+ var onShowCount = 0
+ var onAttachCount = 0
+ var onDetachCount = 0
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ onCreateViewCount++
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+
+ override fun onHiddenChanged(hidden: Boolean) {
+ if (hidden) {
+ onHideCount++
+ } else {
+ onShowCount++
+ }
+ super.onHiddenChanged(hidden)
+ }
+
+ override fun onAttach(context: Context) {
+ onAttachCount++
+ super.onAttach(context)
+ }
+
+ override fun onDetach() {
+ onDetachCount++
+ super.onDetach()
+ }
+
+ override fun onDestroyView() {
+ onDestroyViewCount++
+ super.onDestroyView()
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
index e4a3be9..300fad6 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimationTest.kt
@@ -355,8 +355,7 @@
val fm1 = fc1.supportFragmentManager
- val fragment1 = StrictViewFragment()
- fragment1.setLayoutId(R.layout.scene1)
+ val fragment1 = StrictViewFragment(R.layout.scene1)
fm1.beginTransaction()
.add(R.id.fragmentContainer, fragment1, "1")
.commit()
@@ -596,7 +595,7 @@
@Throws(InterruptedException::class)
private fun assertPostponed(fragment: AnimatorFragment, expectedAnimators: Int) {
- assertThat(fragment.mOnCreateViewCalled).isTrue()
+ assertThat(fragment.onCreateViewCalled).isTrue()
assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
assertThat(fragment.requireView().alpha).isWithin(0f).of(0f)
assertThat(fragment.numAnimators).isEqualTo(expectedAnimators)
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
index b96e10b..b71b846 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
@@ -415,8 +415,7 @@
val fm1 = fc1.supportFragmentManager
- val fragment1 = StrictViewFragment()
- fragment1.setLayoutId(R.layout.scene1)
+ val fragment1 = StrictViewFragment(R.layout.scene1)
fm1.beginTransaction()
.add(R.id.fragmentContainer, fragment1, "1")
.setReorderingAllowed(true)
@@ -509,7 +508,7 @@
}
private fun assertPostponed(fragment: AnimatorFragment, expectedAnimators: Int) {
- assertThat(fragment.mOnCreateViewCalled).isTrue()
+ assertThat(fragment.onCreateViewCalled).isTrue()
assertThat(fragment.requireView().visibility).isEqualTo(View.VISIBLE)
assertThat(fragment.requireView().alpha).isWithin(0f).of(0f)
assertThat(fragment.numAnimators).isEqualTo(expectedAnimators)
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java b/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java
deleted file mode 100644
index a52ef71..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.java
+++ /dev/null
@@ -1,2038 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app;
-
-import static androidx.fragment.app.FragmentTestUtil.HostCallbacks;
-import static androidx.fragment.app.FragmentTestUtil.restartFragmentController;
-import static androidx.fragment.app.FragmentTestUtil.shutdownFragmentController;
-import static androidx.fragment.app.FragmentTestUtil.startupFragmentController;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-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.assertNotSame;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.util.AttributeSet;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.view.ViewCompat;
-import androidx.fragment.app.test.EmptyFragmentTestActivity;
-import androidx.fragment.app.test.FragmentTestActivity;
-import androidx.fragment.test.R;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.ViewModelStore;
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.MediumTest;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-@MediumTest
-public class FragmentLifecycleTest {
-
- @Rule
- public ActivityTestRule<EmptyFragmentTestActivity> mActivityRule =
- new ActivityTestRule<EmptyFragmentTestActivity>(EmptyFragmentTestActivity.class);
-
- @Test
- public void basicLifecycle() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictFragment strictFragment = new StrictFragment();
-
- // Add fragment; StrictFragment will throw if it detects any violation
- // in standard lifecycle method ordering or expected preconditions.
- fm.beginTransaction().add(strictFragment, "EmptyHeadless").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment is not added", strictFragment.isAdded());
- assertFalse("fragment is detached", strictFragment.isDetached());
- assertTrue("fragment is not resumed", strictFragment.isResumed());
- Lifecycle lifecycle = strictFragment.getLifecycle();
- assertThat(lifecycle.getCurrentState())
- .isEqualTo(Lifecycle.State.RESUMED);
-
- // Test removal as well; StrictFragment will throw here too.
- fm.beginTransaction().remove(strictFragment).commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment is added", strictFragment.isAdded());
- assertFalse("fragment is resumed", strictFragment.isResumed());
- assertThat(lifecycle.getCurrentState())
- .isEqualTo(Lifecycle.State.DESTROYED);
- // Once removed, a new Lifecycle should be created just in case
- // the developer reuses the same Fragment
- assertThat(strictFragment.getLifecycle().getCurrentState())
- .isEqualTo(Lifecycle.State.INITIALIZED);
-
- // This one is perhaps counterintuitive; "detached" means specifically detached
- // but still managed by a FragmentManager. The .remove call above
- // should not enter this state.
- assertFalse("fragment is detached", strictFragment.isDetached());
- }
-
- @Test
- public void detachment() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictFragment f1 = new StrictFragment();
- final StrictFragment f2 = new StrictFragment();
-
- fm.beginTransaction().add(f1, "1").add(f2, "2").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
- assertTrue("fragment 2 is not added", f2.isAdded());
-
- // Test detaching fragments using StrictFragment to throw on errors.
- fm.beginTransaction().detach(f1).detach(f2).commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not detached", f1.isDetached());
- assertTrue("fragment 2 is not detached", f2.isDetached());
- assertFalse("fragment 1 is added", f1.isAdded());
- assertFalse("fragment 2 is added", f2.isAdded());
-
- // Only reattach f1; leave v2 detached.
- fm.beginTransaction().attach(f1).commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
- assertFalse("fragment 1 is detached", f1.isDetached());
- assertTrue("fragment 2 is not detached", f2.isDetached());
-
- // Remove both from the FragmentManager.
- fm.beginTransaction().remove(f1).remove(f2).commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment 1 is added", f1.isAdded());
- assertFalse("fragment 2 is added", f2.isAdded());
- assertFalse("fragment 1 is detached", f1.isDetached());
- assertFalse("fragment 2 is detached", f2.isDetached());
- }
-
- @Test
- public void basicBackStack() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictFragment f1 = new StrictFragment();
- final StrictFragment f2 = new StrictFragment();
-
- // Add a fragment normally to set up
- fm.beginTransaction().add(f1, "1").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
-
- // Remove the first one and add a second. We're not using replace() here since
- // these fragments are headless and as of this test writing, replace() only works
- // for fragments with views and a container view id.
- // Add it to the back stack so we can pop it afterwards.
- fm.beginTransaction().remove(f1).add(f2, "2").addToBackStack("stack1").commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment 1 is added", f1.isAdded());
- assertTrue("fragment 2 is not added", f2.isAdded());
-
- // Test popping the stack
- fm.popBackStack();
- executePendingTransactions(fm);
-
- assertFalse("fragment 2 is added", f2.isAdded());
- assertTrue("fragment 1 is not added", f1.isAdded());
- }
-
- @Test
- public void attachBackStack() throws Throwable {
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictFragment f1 = new StrictFragment();
- final StrictFragment f2 = new StrictFragment();
-
- // Add a fragment normally to set up
- fm.beginTransaction().add(f1, "1").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
-
- fm.beginTransaction().detach(f1).add(f2, "2").addToBackStack("stack1").commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not detached", f1.isDetached());
- assertFalse("fragment 2 is detached", f2.isDetached());
- assertFalse("fragment 1 is added", f1.isAdded());
- assertTrue("fragment 2 is not added", f2.isAdded());
- }
-
- @Test
- public void viewLifecycle() throws Throwable {
- // Test basic lifecycle when the fragment creates a view
-
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment f1 = new StrictViewFragment();
-
- fm.beginTransaction().add(android.R.id.content, f1).commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
- final View view = f1.getView();
- assertNotNull("fragment 1 returned null from getView", view);
- assertTrue("fragment 1's view is not attached to a window",
- ViewCompat.isAttachedToWindow(view));
-
- fm.beginTransaction().remove(f1).commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment 1 is added", f1.isAdded());
- assertNull("fragment 1 returned non-null from getView after removal", f1.getView());
- assertFalse("fragment 1's previous view is still attached to a window",
- ViewCompat.isAttachedToWindow(view));
- }
-
- @Test
- public void viewReplace() throws Throwable {
- // Replace one view with another, then reverse it with the back stack
-
- final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
- final StrictViewFragment f1 = new StrictViewFragment();
- final StrictViewFragment f2 = new StrictViewFragment();
-
- fm.beginTransaction().add(android.R.id.content, f1).commit();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
-
- View origView1 = f1.getView();
- assertNotNull("fragment 1 returned null view", origView1);
- assertTrue("fragment 1's view not attached", ViewCompat.isAttachedToWindow(origView1));
-
- fm.beginTransaction().replace(android.R.id.content, f2).addToBackStack("stack1").commit();
- executePendingTransactions(fm);
-
- assertFalse("fragment 1 is added", f1.isAdded());
- assertTrue("fragment 2 is added", f2.isAdded());
- assertNull("fragment 1 returned non-null view", f1.getView());
- assertFalse("fragment 1's old view still attached",
- ViewCompat.isAttachedToWindow(origView1));
- View origView2 = f2.getView();
- assertNotNull("fragment 2 returned null view", origView2);
- assertTrue("fragment 2's view not attached", ViewCompat.isAttachedToWindow(origView2));
-
- fm.popBackStack();
- executePendingTransactions(fm);
-
- assertTrue("fragment 1 is not added", f1.isAdded());
- assertFalse("fragment 2 is added", f2.isAdded());
- assertNull("fragment 2 returned non-null view", f2.getView());
- assertFalse("fragment 2's view still attached", ViewCompat.isAttachedToWindow(origView2));
- View newView1 = f1.getView();
- assertNotSame("fragment 1 had same view from last attachment", origView1, newView1);
- assertTrue("fragment 1's view not attached", ViewCompat.isAttachedToWindow(newView1));
- }
-
- @Test
- @UiThreadTest
- public void setInitialSavedState() throws Throwable {
- FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- // Add a StateSaveFragment
- StateSaveFragment fragment = new StateSaveFragment("Saved", "");
- fm.beginTransaction().add(fragment, "tag").commit();
- executePendingTransactions(fm);
-
- // Change the user visible hint before we save state
- fragment.setUserVisibleHint(false);
-
- // Save its state and remove it
- Fragment.SavedState state = fm.saveFragmentInstanceState(fragment);
- fm.beginTransaction().remove(fragment).commit();
- executePendingTransactions(fm);
-
- // Create a new instance, calling setInitialSavedState
- fragment = new StateSaveFragment("", "");
- fragment.setInitialSavedState(state);
-
- // Add the new instance
- fm.beginTransaction().add(fragment, "tag").commit();
- executePendingTransactions(fm);
-
- assertEquals("setInitialSavedState did not restore saved state",
- "Saved", fragment.getSavedState());
- assertEquals("setInitialSavedState did not restore user visible hint",
- false, fragment.getUserVisibleHint());
- }
-
- @Test
- @UiThreadTest
- public void setInitialSavedStateWithSetUserVisibleHint() throws Throwable {
- FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
-
- // Add a StateSaveFragment
- StateSaveFragment fragment = new StateSaveFragment("Saved", "");
- fm.beginTransaction().add(fragment, "tag").commit();
- executePendingTransactions(fm);
-
- // Save its state and remove it
- Fragment.SavedState state = fm.saveFragmentInstanceState(fragment);
- fm.beginTransaction().remove(fragment).commit();
- executePendingTransactions(fm);
-
- // Create a new instance, calling setInitialSavedState
- fragment = new StateSaveFragment("", "");
- fragment.setInitialSavedState(state);
-
- // Change the user visible hint after we call setInitialSavedState
- fragment.setUserVisibleHint(false);
-
- // Add the new instance
- fm.beginTransaction().add(fragment, "tag").commit();
- executePendingTransactions(fm);
-
- assertEquals("setInitialSavedState did not restore saved state",
- "Saved", fragment.getSavedState());
- assertEquals("setUserVisibleHint should override setInitialSavedState",
- false, fragment.getUserVisibleHint());
- }
-
- @Test
- @UiThreadTest
- public void testSavedInstanceStateAfterRestore() {
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- // Add the initial state
- final StrictFragment parentFragment = new StrictFragment();
- parentFragment.setRetainInstance(true);
- final StrictFragment childFragment = new StrictFragment();
- fm1.beginTransaction().add(parentFragment, "parent").commitNow();
- final FragmentManager childFragmentManager = parentFragment.getChildFragmentManager();
- childFragmentManager.beginTransaction().add(childFragment, "child").commitNow();
-
- // Confirm the initial state
- assertWithMessage("Initial parent saved instance state should be null")
- .that(parentFragment.mSavedInstanceState)
- .isNull();
- assertWithMessage("Initial child saved instance state should be null")
- .that(childFragment.mSavedInstanceState)
- .isNull();
-
- // Bring the state back down to destroyed, simulating an activity restart
- fc1.dispatchPause();
- final Parcelable savedState = fc1.saveAllState();
- fc1.dispatchStop();
- fc1.dispatchDestroy();
-
- // Create the new controller and restore state
- final FragmentController fc2 =
- startupFragmentController(mActivityRule.getActivity(), savedState, viewModelStore);
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
-
- final StrictFragment restoredParentFragment = (StrictFragment) fm2
- .findFragmentByTag("parent");
- assertNotNull("Parent fragment was not restored", restoredParentFragment);
- final StrictFragment restoredChildFragment = (StrictFragment) restoredParentFragment
- .getChildFragmentManager().findFragmentByTag("child");
- assertNotNull("Child fragment was not restored", restoredChildFragment);
-
- assertWithMessage("Parent fragment saved instance state should still be null "
- + "since it is a retained Fragment")
- .that(restoredParentFragment.mSavedInstanceState)
- .isNull();
- assertWithMessage("Child fragment saved instance state should be non-null")
- .that(restoredChildFragment.mSavedInstanceState)
- .isNotNull();
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void restoreNestedFragmentsOnBackStack() {
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- // Add the initial state
- final StrictFragment parentFragment = new StrictFragment();
- final StrictFragment childFragment = new StrictFragment();
- fm1.beginTransaction().add(parentFragment, "parent").commitNow();
- final FragmentManager childFragmentManager = parentFragment.getChildFragmentManager();
- childFragmentManager.beginTransaction().add(childFragment, "child").commitNow();
-
- // Now add a Fragment to the back stack
- final StrictFragment replacementChildFragment = new StrictFragment();
- childFragmentManager.beginTransaction()
- .remove(childFragment)
- .add(replacementChildFragment, "child")
- .addToBackStack("back_stack").commit();
- childFragmentManager.executePendingTransactions();
-
- // Move the activity to resumed
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Now bring the state back down
- fc1.dispatchPause();
- final Parcelable savedState = fc1.saveAllState();
- fc1.dispatchStop();
- fc1.dispatchDestroy();
-
- // Create the new controller and restore state
- final FragmentController fc2 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
-
- fc2.attachHost(null);
- fc2.restoreSaveState(savedState);
- fc2.dispatchCreate();
-
- final StrictFragment restoredParentFragment = (StrictFragment) fm2
- .findFragmentByTag("parent");
- assertNotNull("Parent fragment was not restored", restoredParentFragment);
- final StrictFragment restoredChildFragment = (StrictFragment) restoredParentFragment
- .getChildFragmentManager().findFragmentByTag("child");
- assertNotNull("Child fragment was not restored", restoredChildFragment);
-
- fc2.dispatchActivityCreated();
- fc2.noteStateNotSaved();
- fc2.execPendingActions();
- fc2.dispatchStart();
- fc2.dispatchResume();
- fc2.execPendingActions();
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void restoreRetainedInstanceFragments() throws Throwable {
- // Create a new FragmentManager in isolation, nest some assorted fragments
- // and then restore them to a second new FragmentManager.
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- // Configure fragments.
-
- // This retained fragment will be added, then removed. After being removed, it
- // should no longer be retained by the FragmentManager
- final StateSaveFragment removedFragment = new StateSaveFragment("Removed",
- "UnsavedRemoved");
- removedFragment.setRetainInstance(true);
- fm1.beginTransaction().add(removedFragment, "tag:removed").commitNow();
- fm1.beginTransaction().remove(removedFragment).commitNow();
-
- // This retained fragment will be added, then detached. After being detached, it
- // should continue to be retained by the FragmentManager
- final StateSaveFragment detachedFragment = new StateSaveFragment("Detached",
- "UnsavedDetached");
- removedFragment.setRetainInstance(true);
- fm1.beginTransaction().add(detachedFragment, "tag:detached").commitNow();
- fm1.beginTransaction().detach(detachedFragment).commitNow();
-
- // Grandparent fragment will not retain instance
- final StateSaveFragment grandparentFragment = new StateSaveFragment("Grandparent",
- "UnsavedGrandparent");
- assertNotNull("grandparent fragment saved state not initialized",
- grandparentFragment.getSavedState());
- assertNotNull("grandparent fragment unsaved state not initialized",
- grandparentFragment.getUnsavedState());
- fm1.beginTransaction().add(grandparentFragment, "tag:grandparent").commitNow();
-
- // Parent fragment will retain instance
- final StateSaveFragment parentFragment = new StateSaveFragment("Parent", "UnsavedParent");
- assertNotNull("parent fragment saved state not initialized",
- parentFragment.getSavedState());
- assertNotNull("parent fragment unsaved state not initialized",
- parentFragment.getUnsavedState());
- parentFragment.setRetainInstance(true);
- grandparentFragment.getChildFragmentManager().beginTransaction()
- .add(parentFragment, "tag:parent").commitNow();
- assertSame("parent fragment is not a child of grandparent",
- grandparentFragment, parentFragment.getParentFragment());
-
- // Child fragment will not retain instance
- final StateSaveFragment childFragment = new StateSaveFragment("Child", "UnsavedChild");
- assertNotNull("child fragment saved state not initialized",
- childFragment.getSavedState());
- assertNotNull("child fragment unsaved state not initialized",
- childFragment.getUnsavedState());
- parentFragment.getChildFragmentManager().beginTransaction()
- .add(childFragment, "tag:child").commitNow();
- assertSame("child fragment is not a child of grandpanret",
- parentFragment, childFragment.getParentFragment());
-
- // Saved for comparison later
- final FragmentManager parentChildFragmentManager = parentFragment.getChildFragmentManager();
-
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Bring the state back down to destroyed, simulating an activity restart
- fc1.dispatchPause();
- final Parcelable savedState = fc1.saveAllState();
- fc1.dispatchStop();
- fc1.dispatchDestroy();
-
- // Create the new controller and restore state
- final FragmentController fc2 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
-
- fc2.attachHost(null);
- fc2.restoreSaveState(savedState);
- fc2.dispatchCreate();
-
- // Confirm that the restored fragments are available and in the expected states
- final StateSaveFragment restoredRemovedFragment = (StateSaveFragment)
- fm2.findFragmentByTag("tag:removed");
- assertNull(restoredRemovedFragment);
- assertTrue("Removed Fragment should be destroyed", removedFragment.mCalledOnDestroy);
-
- final StateSaveFragment restoredDetachedFragment = (StateSaveFragment)
- fm2.findFragmentByTag("tag:detached");
- assertNotNull(restoredDetachedFragment);
-
- final StateSaveFragment restoredGrandparent = (StateSaveFragment) fm2.findFragmentByTag(
- "tag:grandparent");
- assertNotNull("grandparent fragment not restored", restoredGrandparent);
-
- assertNotSame("grandparent fragment instance was saved",
- grandparentFragment, restoredGrandparent);
- assertEquals("grandparent fragment saved state was not equal",
- grandparentFragment.getSavedState(), restoredGrandparent.getSavedState());
- assertNotEquals("grandparent fragment unsaved state was unexpectedly preserved",
- grandparentFragment.getUnsavedState(), restoredGrandparent.getUnsavedState());
-
- final StateSaveFragment restoredParent = (StateSaveFragment) restoredGrandparent
- .getChildFragmentManager().findFragmentByTag("tag:parent");
- assertNotNull("parent fragment not restored", restoredParent);
-
- assertSame("parent fragment instance was not saved", parentFragment, restoredParent);
- assertEquals("parent fragment saved state was not equal",
- parentFragment.getSavedState(), restoredParent.getSavedState());
- assertEquals("parent fragment unsaved state was not equal",
- parentFragment.getUnsavedState(), restoredParent.getUnsavedState());
- assertNotSame("parent fragment has the same child FragmentManager",
- parentChildFragmentManager, restoredParent.getChildFragmentManager());
-
- final StateSaveFragment restoredChild = (StateSaveFragment) restoredParent
- .getChildFragmentManager().findFragmentByTag("tag:child");
- assertNotNull("child fragment not restored", restoredChild);
-
- assertNotSame("child fragment instance state was saved", childFragment, restoredChild);
- assertEquals("child fragment saved state was not equal",
- childFragment.getSavedState(), restoredChild.getSavedState());
- assertNotEquals("child fragment saved state was unexpectedly equal",
- childFragment.getUnsavedState(), restoredChild.getUnsavedState());
-
- fc2.dispatchActivityCreated();
- fc2.noteStateNotSaved();
- fc2.execPendingActions();
- fc2.dispatchStart();
- fc2.dispatchResume();
- fc2.execPendingActions();
-
- // Test that the fragments are in the configuration we expect
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
-
- assertTrue("grandparent not destroyed", restoredGrandparent.mCalledOnDestroy);
- assertTrue("parent not destroyed", restoredParent.mCalledOnDestroy);
- assertTrue("child not destroyed", restoredChild.mCalledOnDestroy);
- }
-
- @Test
- @UiThreadTest
- public void restoreRetainedInstanceFragmentWithTransparentActivityConfigChange() {
- // Create a new FragmentManager in isolation, add a retained instance Fragment,
- // then mimic the following scenario:
- // 1. Activity A adds retained Fragment F
- // 2. Activity A starts translucent Activity B
- // 3. Activity B start opaque Activity C
- // 4. Rotate phone
- // 5. Finish Activity C
- // 6. Finish Activity B
-
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- // Add the retained Fragment
- final StateSaveFragment retainedFragment = new StateSaveFragment("Retained",
- "UnsavedRetained");
- retainedFragment.setRetainInstance(true);
- fm1.beginTransaction().add(retainedFragment, "tag:retained").commitNow();
-
- // Move the activity to resumed
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Launch the transparent activity on top
- fc1.dispatchPause();
-
- // Launch the opaque activity on top
- final Parcelable savedState = fc1.saveAllState();
- fc1.dispatchStop();
-
- // Finish the opaque activity, making our Activity visible i.e., started
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
-
- // Finish the transparent activity, causing a config change
- fc1.dispatchStop();
- fc1.dispatchDestroy();
-
- // Create the new controller and restore state
- final FragmentController fc2 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm2 = fc2.getSupportFragmentManager();
-
- fc2.attachHost(null);
- fc2.restoreSaveState(savedState);
- fc2.dispatchCreate();
-
- final StateSaveFragment restoredFragment = (StateSaveFragment) fm2
- .findFragmentByTag("tag:retained");
- assertNotNull("retained fragment not restored", restoredFragment);
- assertEquals("The retained Fragment shouldn't be recreated",
- retainedFragment, restoredFragment);
-
- fc2.dispatchActivityCreated();
- fc2.noteStateNotSaved();
- fc2.execPendingActions();
- fc2.dispatchStart();
- fc2.dispatchResume();
- fc2.execPendingActions();
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void saveAnimationState() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- fm.beginTransaction()
- .setCustomAnimations(0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
- .add(android.R.id.content, SimpleFragment.create(R.layout.fragment_a))
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
-
- // Causes save and restore of fragments and back stack
- fc = restartFragmentController(mActivityRule.getActivity(), fc, viewModelStore);
- fm = fc.getSupportFragmentManager();
-
- assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
-
- fm.beginTransaction()
- .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, 0, 0)
- .replace(android.R.id.content, SimpleFragment.create(R.layout.fragment_b))
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0);
-
- // Causes save and restore of fragments and back stack
- fc = restartFragmentController(mActivityRule.getActivity(), fc, viewModelStore);
- fm = fc.getSupportFragmentManager();
-
- assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0);
-
- fm.popBackStackImmediate();
-
- assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
-
- shutdownFragmentController(fc, viewModelStore);
- }
-
- /**
- * This test confirms that as long as a parent fragment has called super.onCreate,
- * any child fragments added, committed and with transactions executed will be brought
- * to at least the CREATED state by the time the parent fragment receives onCreateView.
- * This means the child fragment will have received onAttach/onCreate.
- */
- @Test
- @UiThreadTest
- public void childFragmentManagerAttach() throws Throwable {
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
- fc.attachHost(null);
- fc.dispatchCreate();
-
- FragmentManager.FragmentLifecycleCallbacks
- mockLc = mock(FragmentManager.FragmentLifecycleCallbacks.class);
- FragmentManager.FragmentLifecycleCallbacks
- mockRecursiveLc = mock(FragmentManager.FragmentLifecycleCallbacks.class);
-
- FragmentManager fm = fc.getSupportFragmentManager();
- fm.registerFragmentLifecycleCallbacks(mockLc, false);
- fm.registerFragmentLifecycleCallbacks(mockRecursiveLc, true);
-
- ChildFragmentManagerFragment fragment = new ChildFragmentManagerFragment();
- fm.beginTransaction()
- .add(android.R.id.content, fragment)
- .commitNow();
-
- verify(mockLc, times(1)).onFragmentCreated(fm, fragment, null);
-
- fc.dispatchActivityCreated();
-
- Fragment childFragment = fragment.getChildFragment();
-
- verify(mockLc, times(1)).onFragmentActivityCreated(fm, fragment, null);
- verify(mockRecursiveLc, times(1)).onFragmentActivityCreated(fm, fragment, null);
- verify(mockRecursiveLc, times(1)).onFragmentActivityCreated(fm, childFragment, null);
-
- fc.dispatchStart();
-
- verify(mockLc, times(1)).onFragmentStarted(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentStarted(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentStarted(fm, childFragment);
-
- fc.dispatchResume();
-
- verify(mockLc, times(1)).onFragmentResumed(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentResumed(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentResumed(fm, childFragment);
-
- // Confirm that the parent fragment received onAttachFragment
- assertTrue("parent fragment did not receive onAttachFragment",
- fragment.mCalledOnAttachFragment);
-
- fc.dispatchStop();
-
- verify(mockLc, times(1)).onFragmentStopped(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentStopped(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentStopped(fm, childFragment);
-
- viewModelStore.clear();
- fc.dispatchDestroy();
-
- verify(mockLc, times(1)).onFragmentDestroyed(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentDestroyed(fm, fragment);
- verify(mockRecursiveLc, times(1)).onFragmentDestroyed(fm, childFragment);
- }
-
- /**
- * This test checks that FragmentLifecycleCallbacks are invoked when expected.
- */
- @Test
- @UiThreadTest
- public void fragmentLifecycleCallbacks() throws Throwable {
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
- fc.attachHost(null);
- fc.dispatchCreate();
-
- FragmentManager fm = fc.getSupportFragmentManager();
-
- ChildFragmentManagerFragment fragment = new ChildFragmentManagerFragment();
- fm.beginTransaction()
- .add(android.R.id.content, fragment)
- .commitNow();
-
- fc.dispatchActivityCreated();
-
- fc.dispatchStart();
- fc.dispatchResume();
-
- // Confirm that the parent fragment received onAttachFragment
- assertTrue("parent fragment did not receive onAttachFragment",
- fragment.mCalledOnAttachFragment);
-
- shutdownFragmentController(fc, viewModelStore);
- }
-
- /**
- * This tests that fragments call onDestroy when the activity finishes.
- */
- @Test
- @UiThreadTest
- public void fragmentDestroyedOnFinish() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- StrictViewFragment fragmentA = StrictViewFragment.create(R.layout.fragment_a);
- StrictViewFragment fragmentB = StrictViewFragment.create(R.layout.fragment_b);
- fm.beginTransaction()
- .add(android.R.id.content, fragmentA)
- .commit();
- fm.executePendingTransactions();
- fm.beginTransaction()
- .replace(android.R.id.content, fragmentB)
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- shutdownFragmentController(fc, viewModelStore);
- assertTrue(fragmentB.mCalledOnDestroy);
- assertTrue(fragmentA.mCalledOnDestroy);
- }
-
- // Make sure that executing transactions during activity lifecycle events
- // is properly prevented.
- @Test
- public void preventReentrantCalls() throws Throwable {
- testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.CREATED);
- testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ACTIVITY_CREATED);
- testLifecycleTransitionFailure(StrictFragment.ACTIVITY_CREATED, StrictFragment.STARTED);
- testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.RESUMED);
-
- testLifecycleTransitionFailure(StrictFragment.RESUMED, StrictFragment.STARTED);
- testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.CREATED);
- testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ATTACHED);
- testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.DETACHED);
- }
-
- private void testLifecycleTransitionFailure(final int fromState,
- final int toState) throws Throwable {
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- final ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = startupFragmentController(
- mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- final Fragment reentrantFragment = ReentrantFragment.create(fromState, toState);
-
- fm1.beginTransaction()
- .add(reentrantFragment, "reentrant")
- .commit();
- try {
- fm1.executePendingTransactions();
- } catch (IllegalStateException e) {
- fail("An exception shouldn't happen when initially adding the fragment");
- }
-
- // Now shut down the fragment controller. When fromState > toState, this should
- // result in an exception
- Parcelable savedState;
- try {
- fc1.dispatchPause();
- savedState = fc1.saveAllState();
- fc1.dispatchStop();
- fc1.dispatchDestroy();
- if (fromState > toState) {
- fail("Expected IllegalStateException when moving from "
- + StrictFragment.stateToString(fromState) + " to "
- + StrictFragment.stateToString(toState));
- }
- } catch (IllegalStateException e) {
- if (fromState < toState) {
- fail("Unexpected IllegalStateException when moving from "
- + StrictFragment.stateToString(fromState) + " to "
- + StrictFragment.stateToString(toState));
- }
- return; // test passed!
- }
-
- // now restore from saved state. This will be reached when
- // fromState < toState. We want to catch the fragment while it
- // is being restored as the fragment controller state is being brought up.
-
- try {
- startupFragmentController(mActivityRule.getActivity(), savedState,
- viewModelStore);
-
- fail("Expected IllegalStateException when moving from "
- + StrictFragment.stateToString(fromState) + " to "
- + StrictFragment.stateToString(toState));
- } catch (IllegalStateException e) {
- // expected, so the test passed!
- }
- }
- });
- }
-
- /**
- * Test to ensure that when dispatch* is called that the fragment manager
- * doesn't cause the contained fragment states to change even if no state changes.
- */
- @Test
- @UiThreadTest
- public void noPrematureStateChange() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- fm.beginTransaction()
- .add(new StrictFragment(), "1")
- .commitNow();
-
- fc = restartFragmentController(mActivityRule.getActivity(), fc, viewModelStore);
-
- fm = fc.getSupportFragmentManager();
-
- StrictFragment fragment1 = (StrictFragment) fm.findFragmentByTag("1");
- assertWithMessage("Fragment should be resumed after restart")
- .that(fragment1.mCalledOnResume)
- .isTrue();
- fragment1.mCalledOnResume = false;
- fc.dispatchResume();
-
- assertWithMessage("Fragment should not get onResume() after second dispatchResume()")
- .that(fragment1.mCalledOnResume)
- .isFalse();
- }
-
- @Test
- @UiThreadTest
- public void testIsStateSaved() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment f = new StrictFragment();
- fm.beginTransaction()
- .add(f, "1")
- .commitNow();
-
- assertFalse("fragment reported state saved while resumed", f.isStateSaved());
-
- fc.dispatchPause();
- fc.saveAllState();
-
- assertTrue("fragment reported state not saved after saveAllState", f.isStateSaved());
-
- fc.dispatchStop();
-
- assertTrue("fragment reported state not saved after stop", f.isStateSaved());
-
- viewModelStore.clear();
- fc.dispatchDestroy();
-
- assertFalse("fragment reported state saved after destroy", f.isStateSaved());
- }
-
- @Test
- @UiThreadTest
- public void testSetArgumentsLifecycle() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- FragmentController fc = startupFragmentController(mActivityRule.getActivity(), null,
- viewModelStore);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment f = new StrictFragment();
- f.setArguments(new Bundle());
-
- fm.beginTransaction()
- .add(f, "1")
- .commitNow();
-
- f.setArguments(new Bundle());
-
- fc.dispatchPause();
- fc.saveAllState();
-
- boolean threw = false;
- try {
- f.setArguments(new Bundle());
- } catch (IllegalStateException ise) {
- threw = true;
- }
- assertTrue("fragment allowed setArguments after state save", threw);
-
- fc.dispatchStop();
-
- threw = false;
- try {
- f.setArguments(new Bundle());
- } catch (IllegalStateException ise) {
- threw = true;
- }
- assertTrue("fragment allowed setArguments after stop", threw);
-
- viewModelStore.clear();
- fc.dispatchDestroy();
-
- // Fully destroyed, so fragments have been removed.
- f.setArguments(new Bundle());
- }
-
- /*
- * Test that target fragments are in a useful state when we restore them, even if they're
- * on the back stack.
- */
-
- @Test
- @UiThreadTest
- public void targetFragmentRestoreLifecycleStateBackStack() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- final Fragment target = new TargetFragment();
- fm1.beginTransaction().add(target, "target").commitNow();
-
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- fm1.beginTransaction()
- .remove(target)
- .add(referrer, "referrer")
- .addToBackStack(null)
- .commit();
-
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Simulate an activity restart
- final FragmentController fc2 =
- restartFragmentController(mActivityRule.getActivity(), fc1, viewModelStore);
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void targetFragmentRestoreLifecycleStateManagerOrder() throws Throwable {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc1 = FragmentController.createController(
- new HostCallbacks(mActivityRule.getActivity(), viewModelStore));
-
- final FragmentManager fm1 = fc1.getSupportFragmentManager();
-
- fc1.attachHost(null);
- fc1.dispatchCreate();
-
- final Fragment target1 = new TargetFragment();
- final Fragment referrer1 = new ReferrerFragment();
- referrer1.setTargetFragment(target1, 0);
-
- fm1.beginTransaction().add(target1, "target1").add(referrer1, "referrer1").commitNow();
-
- final Fragment target2 = new TargetFragment();
- final Fragment referrer2 = new ReferrerFragment();
- referrer2.setTargetFragment(target2, 0);
-
- // Order shouldn't matter.
- fm1.beginTransaction().add(referrer2, "referrer2").add(target2, "target2").commitNow();
-
- fc1.dispatchActivityCreated();
- fc1.noteStateNotSaved();
- fc1.execPendingActions();
- fc1.dispatchStart();
- fc1.dispatchResume();
- fc1.execPendingActions();
-
- // Simulate an activity restart
- final FragmentController fc2 =
- restartFragmentController(mActivityRule.getActivity(), fc1, viewModelStore);
-
- // Bring the state back down to destroyed before we finish the test
- shutdownFragmentController(fc2, viewModelStore);
- }
-
- @Test
- @UiThreadTest
- public void targetFragmentClearedWhenSetToNull() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- referrer.setTargetFragment(null, 0);
-
- assertWithMessage("Target Fragment should cleared after setTargetFragment with null")
- .that(referrer.getTargetFragment())
- .isNull();
-
- fm.beginTransaction()
- .remove(referrer)
- .commitNow();
-
- assertWithMessage("Target Fragment should still be cleared after being removed")
- .that(referrer.getTargetFragment())
- .isNull();
-
- shutdownFragmentController(fc, viewModelStore);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target Fragment is already
- * attached to a FragmentManager, but the referrer Fragment is not attached.
- */
- @Test
- @UiThreadTest
- public void targetFragmentOnlyTargetAdded() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- // Add just the target Fragment to the FragmentManager
- fm.beginTransaction().add(target, "target").commitNow();
-
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction()
- .remove(referrer)
- .commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being removed")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- shutdownFragmentController(fc, viewModelStore);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target fragment is
- * not retained and the referrer fragment is not retained.
- */
- @Test
- @UiThreadTest
- public void targetFragmentNonRetainedNonRetained() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction()
- .remove(referrer)
- .commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being removed")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- shutdownFragmentController(fc, viewModelStore);
-
- assertWithMessage("Target Fragment should be accessible after destruction")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target fragment is
- * retained and the referrer fragment is not retained.
- */
- @Test
- @UiThreadTest
- public void targetFragmentRetainedNonRetained() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- target.setRetainInstance(true);
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction()
- .remove(referrer)
- .commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being removed")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- shutdownFragmentController(fc, viewModelStore);
-
- assertWithMessage("Target Fragment should be accessible after destruction")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target fragment is
- * not retained and the referrer fragment is retained.
- */
- @Test
- @UiThreadTest
- public void targetFragmentNonRetainedRetained() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- final Fragment referrer = new ReferrerFragment();
- referrer.setTargetFragment(target, 0);
- referrer.setRetainInstance(true);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- // Save the state
- fc.dispatchPause();
- fc.saveAllState();
- fc.dispatchStop();
- fc.dispatchDestroy();
-
- assertWithMessage("Target Fragment should be accessible after target Fragment destruction")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
- }
-
- /**
- * Test the availability of getTargetFragment() when the target fragment is
- * retained and the referrer fragment is also retained.
- */
- @Test
- @UiThreadTest
- public void targetFragmentRetainedRetained() {
- ViewModelStore viewModelStore = new ViewModelStore();
- final FragmentController fc =
- startupFragmentController(mActivityRule.getActivity(), null, viewModelStore);
-
- final FragmentManager fm = fc.getSupportFragmentManager();
-
- final Fragment target = new TargetFragment();
- target.setRetainInstance(true);
- final Fragment referrer = new ReferrerFragment();
- referrer.setRetainInstance(true);
- referrer.setTargetFragment(target, 0);
-
- assertWithMessage("Target Fragment should be accessible before being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow();
-
- assertWithMessage("Target Fragment should be accessible after being added")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
-
- // Save the state
- fc.dispatchPause();
- fc.saveAllState();
- fc.dispatchStop();
- fc.dispatchDestroy();
-
- assertWithMessage("Target Fragment should be accessible after FragmentManager destruction")
- .that(referrer.getTargetFragment())
- .isSameAs(target);
- }
-
- @Test
- public void targetFragmentNoCycles() throws Throwable {
- final Fragment one = new Fragment();
- final Fragment two = new Fragment();
- final Fragment three = new Fragment();
-
- try {
- one.setTargetFragment(two, 0);
- two.setTargetFragment(three, 0);
- three.setTargetFragment(one, 0);
- assertTrue("creating a fragment target cycle did not throw IllegalArgumentException",
- false);
- } catch (IllegalArgumentException e) {
- // Success!
- }
- }
-
- @Test
- public void targetFragmentSetClear() throws Throwable {
- final Fragment one = new Fragment();
- final Fragment two = new Fragment();
-
- one.setTargetFragment(two, 0);
- one.setTargetFragment(null, 0);
- }
-
- /**
- * FragmentActivity should not raise the state of a Fragment while it is being destroyed.
- */
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
- @Test
- public void fragmentActivityFinishEarly() throws Throwable {
- Intent intent = new Intent(mActivityRule.getActivity(), FragmentTestActivity.class);
- intent.putExtra("finishEarly", true);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- FragmentTestActivity activity = (FragmentTestActivity)
- InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
-
- assertTrue(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS));
- }
-
- /**
- * When a fragment is saved in non-config, it should be restored to the same index.
- */
- @Test
- @UiThreadTest
- public void restoreNonConfig() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment backStackRetainedFragment = new StrictFragment();
- backStackRetainedFragment.setRetainInstance(true);
- Fragment fragment1 = new StrictFragment();
- fm.beginTransaction()
- .add(backStackRetainedFragment, "backStack")
- .add(fragment1, "1")
- .setPrimaryNavigationFragment(fragment1)
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- Fragment fragment2 = new StrictFragment();
- fragment2.setRetainInstance(true);
- fragment2.setTargetFragment(fragment1, 0);
- Fragment fragment3 = new StrictFragment();
- fm.beginTransaction()
- .remove(backStackRetainedFragment)
- .remove(fragment1)
- .add(fragment2, "2")
- .add(fragment3, "3")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- boolean foundFragment2 = false;
- for (Fragment fragment : fc.getSupportFragmentManager().getFragments()) {
- if (fragment == fragment2) {
- foundFragment2 = true;
- assertNotNull(fragment.getTargetFragment());
- assertEquals("1", fragment.getTargetFragment().getTag());
- } else {
- assertNotEquals("2", fragment.getTag());
- }
- }
- assertTrue(foundFragment2);
- fc.getSupportFragmentManager().popBackStackImmediate();
- Fragment foundBackStackRetainedFragment = fc.getSupportFragmentManager()
- .findFragmentByTag("backStack");
- assertEquals("Retained Fragment on the back stack was not retained",
- backStackRetainedFragment, foundBackStackRetainedFragment);
- }
-
- /**
- * Check that retained fragments in the backstack correctly restored after two "configChanges"
- */
- @Test
- @UiThreadTest
- public void retainedFragmentInBackstack() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment fragment1 = new StrictFragment();
- fm.beginTransaction()
- .add(fragment1, "1")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- Fragment child = new StrictFragment();
- child.setRetainInstance(true);
- fragment1.getChildFragmentManager().beginTransaction()
- .add(child, "child").commit();
- fragment1.getChildFragmentManager().executePendingTransactions();
-
- Fragment fragment2 = new StrictFragment();
- fm.beginTransaction()
- .remove(fragment1)
- .add(fragment2, "2")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- savedState = FragmentTestUtil.destroy(mActivityRule, fc);
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
- fm.popBackStackImmediate();
- Fragment retainedChild = fm.findFragmentByTag("1")
- .getChildFragmentManager().findFragmentByTag("child");
- assertEquals(child, retainedChild);
- }
-
- /**
- * When a fragment has been optimized out, it state should still be saved during
- * save and restore instance state.
- */
- @Test
- @UiThreadTest
- public void saveRemovedFragment() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- SaveStateFragment fragment1 = SaveStateFragment.create(1);
- fm.beginTransaction()
- .add(android.R.id.content, fragment1, "1")
- .addToBackStack(null)
- .commit();
- SaveStateFragment fragment2 = SaveStateFragment.create(2);
- fm.beginTransaction()
- .replace(android.R.id.content, fragment2, "2")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
- fragment2 = (SaveStateFragment) fm.findFragmentByTag("2");
- assertNotNull(fragment2);
- assertEquals(2, fragment2.getValue());
- fm.popBackStackImmediate();
- fragment1 = (SaveStateFragment) fm.findFragmentByTag("1");
- assertNotNull(fragment1);
- assertEquals(1, fragment1.getValue());
- }
-
- /**
- * When there are no retained instance fragments, the FragmentManagerNonConfig's fragments
- * should be null
- */
- @Test
- @UiThreadTest
- public void nullNonConfig() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- Fragment fragment1 = new StrictFragment();
- fm.beginTransaction()
- .add(fragment1, "1")
- .addToBackStack(null)
- .commit();
- fm.executePendingTransactions();
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
- assertNull(savedState.second);
- }
-
- /**
- * When the FragmentManager state changes, the pending transactions should execute.
- */
- @Test
- @UiThreadTest
- public void runTransactionsOnChange() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- RemoveHelloInOnResume fragment1 = new RemoveHelloInOnResume();
- StrictFragment fragment2 = new StrictFragment();
- fm.beginTransaction()
- .add(fragment1, "1")
- .setReorderingAllowed(false)
- .commit();
- fm.beginTransaction()
- .add(fragment2, "Hello")
- .setReorderingAllowed(false)
- .commit();
- fm.executePendingTransactions();
-
- assertEquals(2, fm.getFragments().size());
- assertTrue(fm.getFragments().contains(fragment1));
- assertTrue(fm.getFragments().contains(fragment2));
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
-
- assertEquals(1, fm.getFragments().size());
- for (Fragment fragment : fm.getFragments()) {
- assertTrue(fragment instanceof RemoveHelloInOnResume);
- }
- }
-
- @Test
- @UiThreadTest
- public void optionsMenu() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- InvalidateOptionFragment fragment = new InvalidateOptionFragment();
- fm.beginTransaction()
- .add(android.R.id.content, fragment)
- .commit();
- fm.executePendingTransactions();
-
- Menu menu = mock(Menu.class);
- fc.dispatchPrepareOptionsMenu(menu);
- assertTrue(fragment.onPrepareOptionsMenuCalled);
- fragment.onPrepareOptionsMenuCalled = false;
- FragmentTestUtil.destroy(mActivityRule, fc);
- fc.dispatchPrepareOptionsMenu(menu);
- assertFalse(fragment.onPrepareOptionsMenuCalled);
- }
-
- /**
- * When a retained instance fragment is saved while in the back stack, it should go
- * through onCreate() when it is popped back.
- */
- @Test
- @UiThreadTest
- public void retainInstanceWithOnCreate() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- OnCreateFragment fragment1 = new OnCreateFragment();
-
- fm.beginTransaction()
- .add(fragment1, "1")
- .commit();
- fm.beginTransaction()
- .remove(fragment1)
- .addToBackStack(null)
- .commit();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
- Pair<Parcelable, FragmentManagerNonConfig> restartState =
- Pair.create(savedState.first, null);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, restartState);
-
- // Save again, but keep the state
- savedState = FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
-
- fm = fc.getSupportFragmentManager();
-
- fm.popBackStackImmediate();
- OnCreateFragment fragment2 = (OnCreateFragment) fm.findFragmentByTag("1");
- assertTrue(fragment2.onCreateCalled);
- fm.popBackStackImmediate();
- }
-
- /**
- * A retained instance fragment should go through onCreate() once, even through save and
- * restore.
- */
- @Test
- @UiThreadTest
- public void retainInstanceOneOnCreate() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- OnCreateFragment fragment = new OnCreateFragment();
-
- fm.beginTransaction()
- .add(fragment, "fragment")
- .commit();
- fm.executePendingTransactions();
-
- fm.beginTransaction()
- .remove(fragment)
- .addToBackStack(null)
- .commit();
-
- assertTrue(fragment.onCreateCalled);
- fragment.onCreateCalled = false;
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
-
- fm.popBackStackImmediate();
- assertFalse(fragment.onCreateCalled);
- }
-
- /**
- * A retained instance fragment added via XML should go through onCreate() once, but should get
- * onInflate calls for each inflation.
- */
- @Test
- @UiThreadTest
- public void retainInstanceLayoutOnInflate() throws Throwable {
- FragmentController fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, null);
- FragmentManager fm = fc.getSupportFragmentManager();
-
- RetainedInflatedParentFragment parentFragment = new RetainedInflatedParentFragment();
-
- fm.beginTransaction()
- .add(android.R.id.content, parentFragment)
- .commit();
- fm.executePendingTransactions();
-
- RetainedInflatedChildFragment childFragment = (RetainedInflatedChildFragment)
- parentFragment.getChildFragmentManager().findFragmentById(R.id.child_fragment);
-
- fm.beginTransaction()
- .remove(parentFragment)
- .addToBackStack(null)
- .commit();
-
- Pair<Parcelable, FragmentManagerNonConfig> savedState =
- FragmentTestUtil.destroy(mActivityRule, fc);
-
- fc = FragmentTestUtil.createController(mActivityRule);
- FragmentTestUtil.resume(mActivityRule, fc, savedState);
- fm = fc.getSupportFragmentManager();
-
- fm.popBackStackImmediate();
-
- parentFragment = (RetainedInflatedParentFragment) fm.findFragmentById(android.R.id.content);
- RetainedInflatedChildFragment childFragment2 = (RetainedInflatedChildFragment)
- parentFragment.getChildFragmentManager().findFragmentById(R.id.child_fragment);
-
- assertEquals("Child Fragment should be retained", childFragment, childFragment2);
- assertEquals("Child Fragment should have onInflate called twice",
- 2, childFragment2.mOnInflateCount);
- }
-
- private void assertAnimationsMatch(FragmentManager fm, int enter, int exit, int popEnter,
- int popExit) {
- FragmentManagerImpl fmImpl = (FragmentManagerImpl) fm;
- BackStackRecord record = fmImpl.mBackStack.get(fmImpl.mBackStack.size() - 1);
-
- Assert.assertEquals(enter, record.mEnterAnim);
- Assert.assertEquals(exit, record.mExitAnim);
- Assert.assertEquals(popEnter, record.mPopEnterAnim);
- Assert.assertEquals(popExit, record.mPopExitAnim);
- }
-
- private void executePendingTransactions(final FragmentManager fm) throws Throwable {
- mActivityRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- fm.executePendingTransactions();
- }
- });
- }
-
- public static class StateSaveFragment extends StrictFragment {
- private static final String STATE_KEY = "state";
-
- private String mSavedState;
- private String mUnsavedState;
-
- public StateSaveFragment() {
- }
-
- public StateSaveFragment(String savedState, String unsavedState) {
- mSavedState = savedState;
- mUnsavedState = unsavedState;
- }
-
- public String getSavedState() {
- return mSavedState;
- }
-
- public String getUnsavedState() {
- return mUnsavedState;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- mSavedState = savedInstanceState.getString(STATE_KEY);
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putString(STATE_KEY, mSavedState);
- }
- }
-
- /**
- * This tests a deliberately odd use of a child fragment, added in onCreateView instead
- * of elsewhere. It simulates creating a UI child fragment added to the view hierarchy
- * created by this fragment.
- */
- public static class ChildFragmentManagerFragment extends StrictFragment {
- private FragmentManager mSavedChildFragmentManager;
- private ChildFragmentManagerChildFragment mChildFragment;
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mSavedChildFragmentManager = getChildFragmentManager();
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- assertSame("child FragmentManagers not the same instance", mSavedChildFragmentManager,
- getChildFragmentManager());
- ChildFragmentManagerChildFragment child =
- (ChildFragmentManagerChildFragment) mSavedChildFragmentManager
- .findFragmentByTag("tag");
- if (child == null) {
- child = new ChildFragmentManagerChildFragment("foo");
- mSavedChildFragmentManager.beginTransaction()
- .add(child, "tag")
- .commitNow();
- assertEquals("argument strings don't match", "foo", child.getString());
- }
- mChildFragment = child;
- return new TextView(container.getContext());
- }
-
- @Nullable
- public Fragment getChildFragment() {
- return mChildFragment;
- }
- }
-
- public static class ChildFragmentManagerChildFragment extends StrictFragment {
- private String mString;
-
- public ChildFragmentManagerChildFragment() {
- }
-
- public ChildFragmentManagerChildFragment(String arg) {
- final Bundle b = new Bundle();
- b.putString("string", arg);
- setArguments(b);
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mString = requireArguments().getString("string", "NO VALUE");
- }
-
- public String getString() {
- return mString;
- }
- }
-
- public static class SimpleFragment extends Fragment {
- private int mLayoutId;
- private static final String LAYOUT_ID = "layoutId";
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- mLayoutId = savedInstanceState.getInt(LAYOUT_ID, mLayoutId);
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(LAYOUT_ID, mLayoutId);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(mLayoutId, container, false);
- }
-
- public static SimpleFragment create(int layoutId) {
- SimpleFragment fragment = new SimpleFragment();
- fragment.mLayoutId = layoutId;
- return fragment;
- }
- }
-
- public static class TargetFragment extends Fragment {
- public boolean calledCreate;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- calledCreate = true;
- }
- }
-
- public static class ReferrerFragment extends Fragment {
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Fragment target = getTargetFragment();
- assertNotNull("target fragment was null during referrer onCreate", target);
-
- if (!(target instanceof TargetFragment)) {
- throw new IllegalStateException("target fragment was not a TargetFragment");
- }
-
- assertTrue("target fragment has not yet been created",
- ((TargetFragment) target).calledCreate);
- }
- }
-
- public static class SaveStateFragment extends Fragment {
- private static final String VALUE_KEY = "SaveStateFragment.mValue";
- private int mValue;
-
- public static SaveStateFragment create(int value) {
- SaveStateFragment saveStateFragment = new SaveStateFragment();
- saveStateFragment.mValue = value;
- return saveStateFragment;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(VALUE_KEY, mValue);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- mValue = savedInstanceState.getInt(VALUE_KEY, mValue);
- }
- }
-
- public int getValue() {
- return mValue;
- }
- }
-
- public static class RemoveHelloInOnResume extends Fragment {
- @Override
- public void onResume() {
- super.onResume();
- Fragment fragment = getFragmentManager().findFragmentByTag("Hello");
- if (fragment != null) {
- getFragmentManager().beginTransaction().remove(fragment).commit();
- }
- }
- }
-
- public static class InvalidateOptionFragment extends Fragment {
- public boolean onPrepareOptionsMenuCalled;
-
- public InvalidateOptionFragment() {
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onPrepareOptionsMenu(Menu menu) {
- onPrepareOptionsMenuCalled = true;
- assertNotNull(getContext());
- super.onPrepareOptionsMenu(menu);
- }
- }
-
- public static class OnCreateFragment extends Fragment {
- public boolean onCreateCalled;
-
- public OnCreateFragment() {
- setRetainInstance(true);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- onCreateCalled = true;
- }
- }
-
- @ContentView(R.layout.nested_retained_inflated_fragment_parent)
- public static class RetainedInflatedParentFragment extends Fragment {
- }
-
- @ContentView(R.layout.nested_inflated_fragment_child)
- public static class RetainedInflatedChildFragment extends Fragment {
-
- int mOnInflateCount = 0;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setRetainInstance(true);
- }
-
- @Override
- public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
- @Nullable Bundle savedInstanceState) {
- super.onInflate(context, attrs, savedInstanceState);
- mOnInflateCount++;
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
new file mode 100644
index 0000000..42870f1
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
@@ -0,0 +1,910 @@
+/*
+ * Copyright 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 androidx.fragment.app
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.util.Pair
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.ContentView
+import androidx.core.view.ViewCompat
+import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
+import androidx.fragment.app.FragmentTestUtil.HostCallbacks
+import androidx.fragment.app.FragmentTestUtil.shutdownFragmentController
+import androidx.fragment.app.FragmentTestUtil.startupFragmentController
+import androidx.fragment.app.test.EmptyFragmentTestActivity
+import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewModelStore
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class FragmentLifecycleTest {
+
+ @get:Rule
+ val activityRule = ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ @Test
+ fun basicLifecycle() {
+ val fm = activityRule.activity.supportFragmentManager
+ val strictFragment = StrictFragment()
+
+ // Add fragment; StrictFragment will throw if it detects any violation
+ // in standard lifecycle method ordering or expected preconditions.
+ fm.beginTransaction().add(strictFragment, "EmptyHeadless").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment is not added").that(strictFragment.isAdded).isTrue()
+ assertWithMessage("fragment is detached").that(strictFragment.isDetached).isFalse()
+ assertWithMessage("fragment is not resumed").that(strictFragment.isResumed).isTrue()
+ val lifecycle = strictFragment.lifecycle
+ assertThat(lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // Test removal as well; StrictFragment will throw here too.
+ fm.beginTransaction().remove(strictFragment).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment is added").that(strictFragment.isAdded).isFalse()
+ assertWithMessage("fragment is resumed").that(strictFragment.isResumed).isFalse()
+ assertThat(lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ // Once removed, a new Lifecycle should be created just in case
+ // the developer reuses the same Fragment
+ assertThat(strictFragment.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+ // This one is perhaps counterintuitive; "detached" means specifically detached
+ // but still managed by a FragmentManager. The .remove call above
+ // should not enter this state.
+ assertWithMessage("fragment is detached").that(strictFragment.isDetached).isFalse()
+ }
+
+ @Test
+ fun detachment() {
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictFragment()
+ val f2 = StrictFragment()
+
+ fm.beginTransaction().add(f1, "1").add(f2, "2").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue()
+
+ // Test detaching fragments using StrictFragment to throw on errors.
+ fm.beginTransaction().detach(f1).detach(f2).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not detached").that(f1.isDetached).isTrue()
+ assertWithMessage("fragment 2 is not detached").that(f2.isDetached).isTrue()
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse()
+
+ // Only reattach f1; leave v2 detached.
+ fm.beginTransaction().attach(f1).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ assertWithMessage("fragment 1 is detached").that(f1.isDetached).isFalse()
+ assertWithMessage("fragment 2 is not detached").that(f2.isDetached).isTrue()
+
+ // Remove both from the FragmentManager.
+ fm.beginTransaction().remove(f1).remove(f2).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse()
+ assertWithMessage("fragment 1 is detached").that(f1.isDetached).isFalse()
+ assertWithMessage("fragment 2 is detached").that(f2.isDetached).isFalse()
+ }
+
+ @Test
+ fun basicBackStack() {
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictFragment()
+ val f2 = StrictFragment()
+
+ // Add a fragment normally to set up
+ fm.beginTransaction().add(f1, "1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+
+ // Remove the first one and add a second. We're not using replace() here since
+ // these fragments are headless and as of this test writing, replace() only works
+ // for fragments with views and a container view id.
+ // Add it to the back stack so we can pop it afterwards.
+ fm.beginTransaction().remove(f1).add(f2, "2").addToBackStack("stack1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue()
+
+ // Test popping the stack
+ fm.popBackStack()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse()
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ }
+
+ @Test
+ fun attachBackStack() {
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictFragment()
+ val f2 = StrictFragment()
+
+ // Add a fragment normally to set up
+ fm.beginTransaction().add(f1, "1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+
+ fm.beginTransaction().detach(f1).add(f2, "2").addToBackStack("stack1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not detached").that(f1.isDetached).isTrue()
+ assertWithMessage("fragment 2 is detached").that(f2.isDetached).isFalse()
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is not added").that(f2.isAdded).isTrue()
+ }
+
+ @Test
+ fun viewLifecycle() {
+ // Test basic lifecycle when the fragment creates a view
+
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictViewFragment()
+
+ fm.beginTransaction().add(android.R.id.content, f1).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ val view = f1.requireView()
+ assertWithMessage("fragment 1 returned null from getView").that(view).isNotNull()
+ assertWithMessage("fragment 1's view is not attached to a window")
+ .that(ViewCompat.isAttachedToWindow(view)).isTrue()
+
+ fm.beginTransaction().remove(f1).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 1 returned non-null from getView after removal")
+ .that(f1.view).isNull()
+ assertWithMessage("fragment 1's previous view is still attached to a window")
+ .that(ViewCompat.isAttachedToWindow(view)).isFalse()
+ }
+
+ @Test
+ fun viewReplace() {
+ // Replace one view with another, then reverse it with the back stack
+
+ val fm = activityRule.activity.supportFragmentManager
+ val f1 = StrictViewFragment()
+ val f2 = StrictViewFragment()
+
+ fm.beginTransaction().add(android.R.id.content, f1).commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+
+ val origView1 = f1.requireView()
+ assertWithMessage("fragment 1 returned null view").that(origView1).isNotNull()
+ assertWithMessage("fragment 1's view not attached")
+ .that(ViewCompat.isAttachedToWindow(origView1)).isTrue()
+
+ fm.beginTransaction().replace(android.R.id.content, f2).addToBackStack("stack1").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is added").that(f1.isAdded).isFalse()
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isTrue()
+ assertWithMessage("fragment 1 returned non-null view").that(f1.view).isNull()
+ assertWithMessage("fragment 1's old view still attached")
+ .that(ViewCompat.isAttachedToWindow(origView1)).isFalse()
+ val origView2 = f2.requireView()
+ assertWithMessage("fragment 2 returned null view").that(origView2).isNotNull()
+ assertWithMessage("fragment 2's view not attached")
+ .that(ViewCompat.isAttachedToWindow(origView2)).isTrue()
+
+ fm.popBackStack()
+ executePendingTransactions(fm)
+
+ assertWithMessage("fragment 1 is not added").that(f1.isAdded).isTrue()
+ assertWithMessage("fragment 2 is added").that(f2.isAdded).isFalse()
+ assertWithMessage("fragment 2 returned non-null view").that(f2.view).isNull()
+ assertWithMessage("fragment 2's view still attached")
+ .that(ViewCompat.isAttachedToWindow(origView2)).isFalse()
+ val newView1 = f1.requireView()
+ assertWithMessage("fragment 1 had same view from last attachment")
+ .that(newView1).isNotSameAs(origView1)
+ assertWithMessage("fragment 1's view not attached")
+ .that(ViewCompat.isAttachedToWindow(newView1)).isTrue()
+ }
+
+ /**
+ * This test confirms that as long as a parent fragment has called super.onCreate,
+ * any child fragments added, committed and with transactions executed will be brought
+ * to at least the CREATED state by the time the parent fragment receives onCreateView.
+ * This means the child fragment will have received onAttach/onCreate.
+ */
+ @Test
+ @UiThreadTest
+ fun childFragmentManagerAttach() {
+ val viewModelStore = ViewModelStore()
+ val fc = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+ fc.attachHost(null)
+ fc.dispatchCreate()
+
+ val mockLc = mock(FragmentManager.FragmentLifecycleCallbacks::class.java)
+ val mockRecursiveLc = mock(FragmentManager.FragmentLifecycleCallbacks::class.java)
+
+ val fm = fc.supportFragmentManager
+ fm.registerFragmentLifecycleCallbacks(mockLc, false)
+ fm.registerFragmentLifecycleCallbacks(mockRecursiveLc, true)
+
+ val fragment = ChildFragmentManagerFragment()
+ fm.beginTransaction()
+ .add(android.R.id.content, fragment)
+ .commitNow()
+
+ verify<FragmentManager.FragmentLifecycleCallbacks>(mockLc, times(1))
+ .onFragmentCreated(fm, fragment, null)
+
+ fc.dispatchActivityCreated()
+
+ val childFragment = fragment.childFragment!!
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1))
+ .onFragmentActivityCreated(fm, fragment, null)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentActivityCreated(fm, fragment, null)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentActivityCreated(fm, childFragment, null)
+
+ fc.dispatchStart()
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentStarted(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentStarted(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentStarted(fm, childFragment)
+
+ fc.dispatchResume()
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentResumed(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentResumed(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentResumed(fm, childFragment)
+
+ // Confirm that the parent fragment received onAttachFragment
+ assertWithMessage("parent fragment did not receive onAttachFragment")
+ .that(fragment.calledOnAttachFragment).isTrue()
+
+ fc.dispatchStop()
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentStopped(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentStopped(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentStopped(fm, childFragment)
+
+ viewModelStore.clear()
+ fc.dispatchDestroy()
+
+ verify<FragmentLifecycleCallbacks>(mockLc, times(1)).onFragmentDestroyed(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentDestroyed(fm, fragment)
+ verify<FragmentLifecycleCallbacks>(mockRecursiveLc, times(1))
+ .onFragmentDestroyed(fm, childFragment)
+ }
+
+ /**
+ * This test checks that FragmentLifecycleCallbacks are invoked when expected.
+ */
+ @Test
+ @UiThreadTest
+ fun fragmentLifecycleCallbacks() {
+ val viewModelStore = ViewModelStore()
+ val fc = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+ fc.attachHost(null)
+ fc.dispatchCreate()
+
+ val fm = fc.supportFragmentManager
+
+ val fragment = ChildFragmentManagerFragment()
+ fm.beginTransaction()
+ .add(android.R.id.content, fragment)
+ .commitNow()
+
+ fc.dispatchActivityCreated()
+
+ fc.dispatchStart()
+ fc.dispatchResume()
+
+ // Confirm that the parent fragment received onAttachFragment
+ assertWithMessage("parent fragment did not receive onAttachFragment")
+ .that(fragment.calledOnAttachFragment).isTrue()
+
+ shutdownFragmentController(fc, viewModelStore)
+ }
+
+ /**
+ * This tests that fragments call onDestroy when the activity finishes.
+ */
+ @Test
+ @UiThreadTest
+ fun fragmentDestroyedOnFinish() {
+ val viewModelStore = ViewModelStore()
+ val fc = startupFragmentController(activityRule.activity, null, viewModelStore)
+ val fm = fc.supportFragmentManager
+
+ val fragmentA = StrictViewFragment(R.layout.fragment_a)
+ val fragmentB = StrictViewFragment(R.layout.fragment_b)
+ fm.beginTransaction()
+ .add(android.R.id.content, fragmentA)
+ .commit()
+ fm.executePendingTransactions()
+ fm.beginTransaction()
+ .replace(android.R.id.content, fragmentB)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ shutdownFragmentController(fc, viewModelStore)
+ assertThat(fragmentB.calledOnDestroy).isTrue()
+ assertThat(fragmentA.calledOnDestroy).isTrue()
+ }
+
+ // Make sure that executing transactions during activity lifecycle events
+ // is properly prevented.
+ @Test
+ fun preventReentrantCalls() {
+ testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.CREATED)
+ testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ACTIVITY_CREATED)
+ testLifecycleTransitionFailure(StrictFragment.ACTIVITY_CREATED, StrictFragment.STARTED)
+ testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.RESUMED)
+
+ testLifecycleTransitionFailure(StrictFragment.RESUMED, StrictFragment.STARTED)
+ testLifecycleTransitionFailure(StrictFragment.STARTED, StrictFragment.CREATED)
+ testLifecycleTransitionFailure(StrictFragment.CREATED, StrictFragment.ATTACHED)
+ testLifecycleTransitionFailure(StrictFragment.ATTACHED, StrictFragment.DETACHED)
+ }
+
+ private fun testLifecycleTransitionFailure(fromState: Int, toState: Int) {
+ activityRule.runOnUiThread(Runnable {
+ val viewModelStore = ViewModelStore()
+ val fc1 = startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm1 = fc1.supportFragmentManager
+
+ val reentrantFragment = ReentrantFragment.create(fromState, toState)
+
+ fm1.beginTransaction().add(reentrantFragment, "reentrant").commit()
+ try {
+ fm1.executePendingTransactions()
+ } catch (e: IllegalStateException) {
+ fail("An exception shouldn't happen when initially adding the fragment")
+ }
+
+ // Now shut down the fragment controller. When fromState > toState, this should
+ // result in an exception
+ val savedState: Parcelable?
+ try {
+ fc1.dispatchPause()
+ savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+ if (fromState > toState) {
+ fail(
+ "Expected IllegalStateException when moving from " +
+ StrictFragment.stateToString(fromState) + " to " +
+ StrictFragment.stateToString(toState)
+ )
+ }
+ } catch (e: IllegalStateException) {
+ if (fromState < toState) {
+ fail(
+ "Unexpected IllegalStateException when moving from " +
+ StrictFragment.stateToString(fromState) + " to " +
+ StrictFragment.stateToString(toState)
+ )
+ }
+ assertThat(e)
+ .hasMessageThat().contains("FragmentManager is already executing transactions")
+ return@Runnable // test passed!
+ }
+
+ // now restore from saved state. This will be reached when
+ // fromState < toState. We want to catch the fragment while it
+ // is being restored as the fragment controller state is being brought up.
+
+ try {
+ startupFragmentController(activityRule.activity, savedState, viewModelStore)
+ fail(
+ "Expected IllegalStateException when moving from " +
+ StrictFragment.stateToString(fromState) + " to " +
+ StrictFragment.stateToString(toState)
+ )
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("FragmentManager is already executing transactions")
+ }
+ })
+ }
+
+ @Test
+ @UiThreadTest
+ fun testSetArgumentsLifecycle() {
+ val viewModelStore = ViewModelStore()
+ val fc = startupFragmentController(activityRule.activity, null, viewModelStore)
+ val fm = fc.supportFragmentManager
+
+ val f = StrictFragment()
+ f.arguments = Bundle()
+
+ fm.beginTransaction().add(f, "1").commitNow()
+
+ f.arguments = Bundle()
+
+ fc.dispatchPause()
+ fc.saveAllState()
+
+ try {
+ f.arguments = Bundle()
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("Fragment already added and state has been saved")
+ }
+
+ fc.dispatchStop()
+
+ try {
+ f.arguments = Bundle()
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("Fragment already added and state has been saved")
+ }
+
+ viewModelStore.clear()
+ fc.dispatchDestroy()
+
+ // Fully destroyed, so fragments have been removed.
+ f.arguments = Bundle()
+ }
+
+ /**
+ * FragmentActivity should not raise the state of a Fragment while it is being destroyed.
+ */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
+ @Test
+ fun fragmentActivityFinishEarly() {
+ val intent = Intent(activityRule.activity, FragmentTestActivity::class.java)
+ intent.putExtra("finishEarly", true)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+ val activity = InstrumentationRegistry.getInstrumentation()
+ .startActivitySync(intent) as FragmentTestActivity
+
+ assertThat(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+ }
+
+ /**
+ * When a fragment is saved in non-config, it should be restored to the same index.
+ */
+ @Test
+ @UiThreadTest
+ fun restoreNonConfig() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ val fm = fc.supportFragmentManager
+
+ val backStackRetainedFragment = StrictFragment()
+ backStackRetainedFragment.retainInstance = true
+ val fragment1 = StrictFragment()
+ fm.beginTransaction()
+ .add(backStackRetainedFragment, "backStack")
+ .add(fragment1, "1")
+ .setPrimaryNavigationFragment(fragment1)
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+ val fragment2 = StrictFragment()
+ fragment2.retainInstance = true
+ fragment2.setTargetFragment(fragment1, 0)
+ val fragment3 = StrictFragment()
+ fm.beginTransaction()
+ .remove(backStackRetainedFragment)
+ .remove(fragment1)
+ .add(fragment2, "2")
+ .add(fragment3, "3")
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ var foundFragment2 = false
+ for (fragment in fc.supportFragmentManager.fragments) {
+ if (fragment === fragment2) {
+ foundFragment2 = true
+ assertThat(fragment.getTargetFragment()).isNotNull()
+ assertThat(fragment.getTargetFragment()!!.tag).isEqualTo("1")
+ } else {
+ assertThat(fragment.tag).isNotEqualTo("2")
+ }
+ }
+ assertThat(foundFragment2).isTrue()
+ fc.supportFragmentManager.popBackStackImmediate()
+ val foundBackStackRetainedFragment = fc.supportFragmentManager
+ .findFragmentByTag("backStack")
+ assertWithMessage("Retained Fragment on the back stack was not retained")
+ .that(foundBackStackRetainedFragment).isEqualTo(backStackRetainedFragment)
+ }
+
+ /**
+ * Check that retained fragments in the backstack correctly restored after two "configChanges"
+ */
+ @Test
+ @UiThreadTest
+ fun retainedFragmentInBackstack() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ val fragment1 = StrictFragment()
+ fm.beginTransaction().add(fragment1, "1").addToBackStack(null).commit()
+ fm.executePendingTransactions()
+
+ val child = StrictFragment()
+ child.retainInstance = true
+ fragment1.childFragmentManager.beginTransaction().add(child, "child").commit()
+ fragment1.childFragmentManager.executePendingTransactions()
+
+ val fragment2 = StrictFragment()
+ fm.beginTransaction().remove(fragment1).add(fragment2, "2").addToBackStack(null).commit()
+ fm.executePendingTransactions()
+
+ var savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ savedState = FragmentTestUtil.destroy(activityRule, fc)
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+ fm.popBackStackImmediate()
+ val retainedChild = fm.findFragmentByTag("1")!!
+ .childFragmentManager.findFragmentByTag("child")
+ assertThat(retainedChild).isEqualTo(child)
+ }
+
+ /**
+ * When there are no retained instance fragments, the FragmentManagerNonConfig's fragments
+ * should be null
+ */
+ @Test
+ @UiThreadTest
+ fun nullNonConfig() {
+ val fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ val fm = fc.supportFragmentManager
+
+ val fragment1 = StrictFragment()
+ fm.beginTransaction().add(fragment1, "1").addToBackStack(null).commit()
+ fm.executePendingTransactions()
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+ assertThat(savedState.second).isNull()
+ }
+
+ /**
+ * When the FragmentManager state changes, the pending transactions should execute.
+ */
+ @Test
+ @UiThreadTest
+ fun runTransactionsOnChange() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ val fragment1 = RemoveHelloInOnResume()
+ val fragment2 = StrictFragment()
+ fm.beginTransaction().add(fragment1, "1").setReorderingAllowed(false).commit()
+ fm.beginTransaction().add(fragment2, "Hello").setReorderingAllowed(false).commit()
+ fm.executePendingTransactions()
+
+ assertThat(fm.fragments.size).isEqualTo(2)
+ assertThat(fm.fragments.contains(fragment1)).isTrue()
+ assertThat(fm.fragments.contains(fragment2)).isTrue()
+
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+
+ assertThat(fm.fragments.size).isEqualTo(1)
+ for (fragment in fm.fragments) {
+ assertThat(fragment is RemoveHelloInOnResume).isTrue()
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun optionsMenu() {
+ val fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ val fm = fc.supportFragmentManager
+
+ val fragment = InvalidateOptionFragment()
+ fm.beginTransaction().add(android.R.id.content, fragment).commit()
+ fm.executePendingTransactions()
+
+ val menu = mock(Menu::class.java)
+ fc.dispatchPrepareOptionsMenu(menu)
+ assertThat(fragment.onPrepareOptionsMenuCalled).isTrue()
+ fragment.onPrepareOptionsMenuCalled = false
+ FragmentTestUtil.destroy(activityRule, fc)
+ fc.dispatchPrepareOptionsMenu(menu)
+ assertThat(fragment.onPrepareOptionsMenuCalled).isFalse()
+ }
+
+ /**
+ * When a retained instance fragment is saved while in the back stack, it should go
+ * through onCreate() when it is popped back.
+ */
+ @Test
+ @UiThreadTest
+ fun retainInstanceWithOnCreate() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ val fragment1 = OnCreateFragment()
+
+ fm.beginTransaction().add(fragment1, "1").commit()
+ fm.beginTransaction().remove(fragment1).addToBackStack(null).commit()
+
+ var savedState = FragmentTestUtil.destroy(activityRule, fc)
+ val restartState = Pair.create<Parcelable, FragmentManagerNonConfig>(savedState.first, null)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, restartState)
+
+ // Save again, but keep the state
+ savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+
+ fm = fc.supportFragmentManager
+
+ fm.popBackStackImmediate()
+ val fragment2 = fm.findFragmentByTag("1") as OnCreateFragment
+ assertThat(fragment2.onCreateCalled).isTrue()
+ fm.popBackStackImmediate()
+ }
+
+ /**
+ * A retained instance fragment should go through onCreate() once, even through save and
+ * restore.
+ */
+ @Test
+ @UiThreadTest
+ fun retainInstanceOneOnCreate() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ val fragment = OnCreateFragment()
+
+ fm.beginTransaction().add(fragment, "fragment").commit()
+ fm.executePendingTransactions()
+
+ fm.beginTransaction().remove(fragment).addToBackStack(null).commit()
+
+ assertThat(fragment.onCreateCalled).isTrue()
+ fragment.onCreateCalled = false
+
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+
+ fm.popBackStackImmediate()
+ assertThat(fragment.onCreateCalled).isFalse()
+ }
+
+ /**
+ * A retained instance fragment added via XML should go through onCreate() once, but should get
+ * onInflate calls for each inflation.
+ */
+ @Test
+ @UiThreadTest
+ fun retainInstanceLayoutOnInflate() {
+ var fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ var parentFragment = RetainedInflatedParentFragment()
+
+ fm.beginTransaction().add(android.R.id.content, parentFragment).commit()
+ fm.executePendingTransactions()
+
+ val childFragment = parentFragment.childFragmentManager
+ .findFragmentById(R.id.child_fragment) as RetainedInflatedChildFragment
+
+ fm.beginTransaction().remove(parentFragment).addToBackStack(null).commit()
+
+ val savedState = FragmentTestUtil.destroy(activityRule, fc)
+
+ fc = FragmentTestUtil.createController(activityRule)
+ FragmentTestUtil.resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+
+ fm.popBackStackImmediate()
+
+ parentFragment = fm.findFragmentById(android.R.id.content) as RetainedInflatedParentFragment
+ val childFragment2 = parentFragment.childFragmentManager
+ .findFragmentById(R.id.child_fragment) as RetainedInflatedChildFragment
+
+ assertWithMessage("Child Fragment should be retained")
+ .that(childFragment2).isEqualTo(childFragment)
+ assertWithMessage("Child Fragment should have onInflate called twice")
+ .that(childFragment2.mOnInflateCount).isEqualTo(2)
+ }
+
+ private fun executePendingTransactions(fm: FragmentManager) {
+ activityRule.runOnUiThread { fm.executePendingTransactions() }
+ }
+
+ /**
+ * This tests a deliberately odd use of a child fragment, added in onCreateView instead
+ * of elsewhere. It simulates creating a UI child fragment added to the view hierarchy
+ * created by this fragment.
+ */
+ class ChildFragmentManagerFragment : StrictFragment() {
+ private lateinit var savedChildFragmentManager: FragmentManager
+ private lateinit var childFragmentManagerChildFragment: ChildFragmentManagerChildFragment
+
+ val childFragment: Fragment?
+ get() = childFragmentManagerChildFragment
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ savedChildFragmentManager = childFragmentManager
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ) = TextView(inflater.context).also {
+ assertWithMessage("child FragmentManagers not the same instance")
+ .that(childFragmentManager).isSameAs(savedChildFragmentManager)
+ var child = savedChildFragmentManager
+ .findFragmentByTag("tag") as ChildFragmentManagerChildFragment?
+ if (child == null) {
+ child = ChildFragmentManagerChildFragment("foo")
+ savedChildFragmentManager.beginTransaction().add(child, "tag").commitNow()
+ assertWithMessage("argument strings don't match")
+ .that(child.string).isEqualTo("foo")
+ }
+ childFragmentManagerChildFragment = child
+ }
+ }
+
+ class ChildFragmentManagerChildFragment : StrictFragment {
+ lateinit var string: String
+ private set
+
+ constructor()
+
+ constructor(arg: String) {
+ val b = Bundle()
+ b.putString("string", arg)
+ arguments = b
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ string = requireArguments().getString("string", "NO VALUE")
+ }
+ }
+
+ class RemoveHelloInOnResume : Fragment() {
+ override fun onResume() {
+ super.onResume()
+ val fragment = fragmentManager!!.findFragmentByTag("Hello")
+ if (fragment != null) {
+ fragmentManager!!.beginTransaction().remove(fragment).commit()
+ }
+ }
+ }
+
+ class InvalidateOptionFragment : Fragment() {
+ var onPrepareOptionsMenuCalled: Boolean = false
+
+ init {
+ setHasOptionsMenu(true)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ onPrepareOptionsMenuCalled = true
+ assertThat(context).isNotNull()
+ super.onPrepareOptionsMenu(menu)
+ }
+ }
+
+ class OnCreateFragment : Fragment() {
+ var onCreateCalled: Boolean = false
+
+ init {
+ retainInstance = true
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ onCreateCalled = true
+ }
+ }
+
+ @ContentView(R.layout.nested_retained_inflated_fragment_parent)
+ class RetainedInflatedParentFragment : Fragment()
+
+ @ContentView(R.layout.nested_inflated_fragment_child)
+ class RetainedInflatedChildFragment : Fragment() {
+ internal var mOnInflateCount = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ retainInstance = true
+ }
+
+ override fun onInflate(context: Context, attrs: AttributeSet, savedInstanceState: Bundle?) {
+ super.onInflate(context, attrs, savedInstanceState)
+ mOnInflateCount++
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.kt
index 57cd568..6877430 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReorderingTest.kt
@@ -614,8 +614,7 @@
assertThat(firstEditText.isFocused).isTrue()
val fragment1 = CountCallsFragment()
- val fragment2 = CountCallsFragment()
- fragment2.setLayoutId(R.layout.with_edit_text)
+ val fragment2 = CountCallsFragment(R.layout.with_edit_text)
fm.beginTransaction()
.add(R.id.fragmentContainer2, fragment1)
.addToBackStack(null)
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
index 1430927..2284c44 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
@@ -19,7 +19,6 @@
import android.view.KeyEvent
import android.view.View
import androidx.fragment.app.test.FragmentTestActivity
-import androidx.fragment.app.test.FragmentTestActivity.TestFragment
import androidx.fragment.test.R
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -53,7 +52,7 @@
val fm = activity.supportFragmentManager
fm.beginTransaction()
- .add(R.id.content, TestFragment.create(R.layout.fragment_a))
+ .add(R.id.content, StrictViewFragment(R.layout.fragment_a))
.addToBackStack(null)
.commit()
executePendingTransactions(fm)
@@ -62,7 +61,7 @@
assertThat(activity.findViewById<View>(R.id.textC)).isNull()
fm.beginTransaction()
- .add(R.id.content, TestFragment.create(R.layout.fragment_b))
+ .add(R.id.content, StrictViewFragment(R.layout.fragment_b))
.addToBackStack(null)
.commit()
executePendingTransactions(fm)
@@ -71,7 +70,7 @@
assertThat(activity.findViewById<View>(R.id.textC)).isNull()
activity.supportFragmentManager.beginTransaction()
- .replace(R.id.content, TestFragment.create(R.layout.fragment_c))
+ .replace(R.id.content, StrictViewFragment(R.layout.fragment_c))
.addToBackStack(null)
.commit()
executePendingTransactions(fm)
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
index bc68d97..83d8465 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
@@ -113,8 +113,7 @@
val fragment1 = setupInitialFragment()
// Now do a transition to scene2
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
verifyTransition(fragment1, fragment2, "blueSquare")
@@ -127,13 +126,11 @@
fun intermediateFragment() {
val fragment1 = setupInitialFragment()
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene3)
+ val fragment2 = TransitionFragment(R.layout.scene3)
verifyTransition(fragment1, fragment2, "shared")
- val fragment3 = TransitionFragment()
- fragment3.setLayoutId(R.layout.scene2)
+ val fragment3 = TransitionFragment(R.layout.scene2)
verifyTransition(fragment2, fragment3, "blueSquare")
@@ -149,8 +146,7 @@
val startBlue = findBlue()
val startGreen = findGreen()
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
instrumentation.runOnMainSync {
fragmentManager.beginTransaction()
@@ -190,10 +186,8 @@
@Test
fun crossContainer() {
FragmentTestUtil.setContentView(activityRule, R.layout.double_container)
- val fragment1 = TransitionFragment()
- fragment1.setLayoutId(R.layout.scene1)
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene1)
+ val fragment1 = TransitionFragment(R.layout.scene1)
+ val fragment2 = TransitionFragment(R.layout.scene1)
fragmentManager.beginTransaction()
.setReorderingAllowed(reorderingAllowed)
.add(R.id.fragmentContainer1, fragment1)
@@ -229,8 +223,7 @@
val fragment1 = setupInitialFragment()
// Now do a transition to scene2
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
val enterCallback = mock(SharedElementCallback::class.java)
fragment2.setEnterSharedElementCallback(enterCallback)
@@ -297,8 +290,7 @@
val fragment1 = setupInitialFragment()
// Now do a transition to scene2
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
val startBlue = findBlue()
val startGreen = findGreen()
@@ -372,8 +364,7 @@
val fragment1 = setupInitialFragment()
// Now do a transition to scene2
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
val startBlue = findBlue()
val startBlueBounds = getBoundsOnScreen(startBlue)
@@ -447,7 +438,6 @@
// Now do a transition to scene2
val fragment2 = ComplexTransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
val startBlue = findBlue()
val startGreen = findGreen()
@@ -506,8 +496,7 @@
val fragment1 = setupInitialFragment()
// Now do a transition to scene2
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
verifyTransition(fragment1, fragment2, "blueSquare")
assertThat(fragment1.exitTransition.getTargets().size).isEqualTo(0)
@@ -532,8 +521,7 @@
@Test
fun showHideTransition() {
val fragment1 = setupInitialFragment()
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
val startBlue = findBlue()
val startGreen = findGreen()
@@ -583,8 +571,7 @@
@Test
fun attachDetachTransition() {
val fragment1 = setupInitialFragment()
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
val startBlue = findBlue()
val startGreen = findGreen()
@@ -627,8 +614,7 @@
val fragment1 = setupInitialFragment()
// Now do a transition to scene2
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
val startBlue = findBlue()
val startGreen = findGreen()
@@ -697,7 +683,6 @@
}
// enter transition
val fragment = InvisibleFragment()
- fragment.setLayoutId(R.layout.scene1)
fragmentManager.beginTransaction()
.setReorderingAllowed(reorderingAllowed)
.add(R.id.fragmentContainer, fragment)
@@ -737,8 +722,7 @@
val startGreen = findGreen()
val startBlueBounds = getBoundsOnScreen(startBlue)
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene2)
+ val fragment2 = TransitionFragment(R.layout.scene2)
fragmentManager.beginTransaction()
.setReorderingAllowed(reorderingAllowed)
@@ -758,8 +742,7 @@
verifyNoOtherTransitions(fragment1)
verifyNoOtherTransitions(fragment2)
- val fragment3 = TransitionFragment()
- fragment3.setLayoutId(R.layout.scene3)
+ val fragment3 = TransitionFragment(R.layout.scene3)
activityRule.runOnUiThread {
val fm = activityRule.activity.supportFragmentManager
@@ -801,8 +784,7 @@
val startGreen = findGreen()
val startGreenBounds = getBoundsOnScreen(startGreen)
- val fragment2 = TransitionFragment()
- fragment2.setLayoutId(R.layout.scene3)
+ val fragment2 = TransitionFragment(R.layout.scene3)
fragmentManager.beginTransaction()
.setReorderingAllowed(reorderingAllowed)
@@ -843,8 +825,7 @@
}
private fun setupInitialFragment(): TransitionFragment {
- val fragment1 = TransitionFragment()
- fragment1.setLayoutId(R.layout.scene1)
+ val fragment1 = TransitionFragment(R.layout.scene1)
fragmentManager.beginTransaction()
.setReorderingAllowed(reorderingAllowed)
.add(R.id.fragmentContainer, fragment1)
@@ -983,10 +964,8 @@
val startNumOnBackStackChanged = onBackStackChangedTimes
val changesPerOperation = if (reorderingAllowed) 1 else 2
- val to1 = TransitionFragment()
- to1.setLayoutId(R.layout.scene2)
- val to2 = TransitionFragment()
- to2.setLayoutId(R.layout.scene2)
+ val to1 = TransitionFragment(R.layout.scene2)
+ val to2 = TransitionFragment(R.layout.scene2)
val fromExit1 = findViewById(from1, R.id.greenSquare)
val fromShared1 = findViewById(from1, R.id.blueSquare)
@@ -1122,7 +1101,7 @@
}
}
- class ComplexTransitionFragment : TransitionFragment() {
+ class ComplexTransitionFragment : TransitionFragment(R.layout.scene2) {
val sharedElementEnterTransition1 = TrackingTransition()
val sharedElementEnterTransition2 = TrackingTransition()
val sharedElementReturnTransition1 = TrackingTransition()
@@ -1145,7 +1124,7 @@
}
}
- class InvisibleFragment : TransitionFragment() {
+ class InvisibleFragment : TransitionFragment(R.layout.scene1) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.visibility = View.INVISIBLE
super.onViewCreated(view, savedInstanceState)
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
index a8379f2..be3612c 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
@@ -56,8 +56,7 @@
val activity = activityRule.activity
val fm = activity.supportFragmentManager
- val fragment = StrictViewFragment()
- fragment.setLayoutId(R.layout.fragment_a)
+ val fragment = StrictViewFragment(R.layout.fragment_a)
fm.beginTransaction().add(R.id.content, fragment).commitNow()
assertThat(fragment.viewLifecycleOwner.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
@@ -108,8 +107,7 @@
val fm = activity.supportFragmentManager
val countDownLatch = CountDownLatch(1)
- val fragment = StrictViewFragment()
- fragment.setLayoutId(R.layout.fragment_a)
+ val fragment = StrictViewFragment(R.layout.fragment_a)
fm.beginTransaction().add(R.id.content, fragment).runOnCommit {
assertThat(fragment.viewLifecycleOwner.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
@@ -124,21 +122,20 @@
val fm = activity.supportFragmentManager
val countDownLatch = CountDownLatch(2)
- val fragment = StrictViewFragment()
- fragment.setLayoutId(R.layout.fragment_a)
+ val fragment = StrictViewFragment(R.layout.fragment_a)
activityRule.runOnUiThread {
fragment.viewLifecycleOwnerLiveData.observe(activity,
Observer { lifecycleOwner ->
if (lifecycleOwner != null) {
assertWithMessage("Fragment View LifecycleOwner should be only be set" +
"after onCreateView()")
- .that(fragment.mOnCreateViewCalled)
+ .that(fragment.onCreateViewCalled)
.isTrue()
countDownLatch.countDown()
} else {
assertWithMessage("Fragment View LifecycleOwner should be set to null" +
" after onDestroyView()")
- .that(fragment.mOnDestroyViewCalled)
+ .that(fragment.onDestroyViewCalled)
.isTrue()
countDownLatch.countDown()
}
@@ -155,8 +152,7 @@
val activity = activityRule.activity
val fm = activity.supportFragmentManager
- val fragment = StrictViewFragment()
- fragment.setLayoutId(R.layout.fragment_a)
+ val fragment = StrictViewFragment(R.layout.fragment_a)
val lifecycleObserver = mock(LifecycleEventObserver::class.java)
lateinit var viewLifecycleOwner: LifecycleOwner
activityRule.runOnUiThread {
@@ -197,7 +193,6 @@
val fm = activity.supportFragmentManager
val fragment = ObservingFragment()
- fragment.setLayoutId(R.layout.fragment_a)
fm.beginTransaction().add(R.id.content, fragment).commitNow()
val viewLifecycleOwner = fragment.viewLifecycleOwner
assertThat(viewLifecycleOwner.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
@@ -225,7 +220,6 @@
val fm = activity.supportFragmentManager
val fragment = ObservingFragment()
- fragment.setLayoutId(R.layout.fragment_a)
fm.beginTransaction().add(R.id.content, fragment).commitNow()
val viewLifecycleOwner = fragment.viewLifecycleOwner
assertThat(viewLifecycleOwner.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
@@ -264,7 +258,7 @@
}
}
- class ObservingFragment : StrictViewFragment() {
+ class ObservingFragment : StrictViewFragment(R.layout.fragment_a) {
val liveData = MutableLiveData<Boolean>()
private val onCreateViewObserver = Observer<Boolean> { }
private val onViewCreatedObserver = Observer<Boolean> { }
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
index 33e0ed38..c1d4d40 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
@@ -783,8 +783,7 @@
fun testReplaceFragment() {
FragmentTestUtil.setContentView(activityRule, R.layout.simple_container)
val fm = activityRule.activity.supportFragmentManager
- val fragmentA = StrictViewFragment()
- fragmentA.setLayoutId(R.layout.text_a)
+ val fragmentA = StrictViewFragment(R.layout.text_a)
fm.beginTransaction()
.add(R.id.fragmentContainer, fragmentA)
@@ -796,8 +795,7 @@
assertThat(findViewById(R.id.textB)).isNull()
assertThat(findViewById(R.id.textC)).isNull()
- val fragmentB = StrictViewFragment()
- fragmentB.setLayoutId(R.layout.text_b)
+ val fragmentB = StrictViewFragment(R.layout.text_b)
fm.beginTransaction()
.add(R.id.fragmentContainer, fragmentB)
.addToBackStack(null)
@@ -807,8 +805,7 @@
assertThat(findViewById(R.id.textB)).isNotNull()
assertThat(findViewById(R.id.textC)).isNull()
- val fragmentC = StrictViewFragment()
- fragmentC.setLayoutId(R.layout.text_c)
+ val fragmentC = StrictViewFragment(R.layout.text_c)
fm.beginTransaction()
.replace(R.id.fragmentContainer, fragmentC)
.addToBackStack(null)
@@ -891,10 +888,8 @@
activityRule.activity.findViewById<View>(R.id.fragmentContainer) as ViewGroup
val fm = activityRule.activity.supportFragmentManager
- val fragment1 = StrictViewFragment()
- fragment1.setLayoutId(R.layout.scene1)
- val fragment2 = StrictViewFragment()
- fragment2.setLayoutId(R.layout.fragment_a)
+ val fragment1 = StrictViewFragment(R.layout.scene1)
+ val fragment2 = StrictViewFragment(R.layout.fragment_a)
activityRule.runOnUiThread {
fm.beginTransaction()
@@ -923,7 +918,6 @@
val fm = activityRule.activity.supportFragmentManager
val fragment1 = ParentFragment()
- fragment1.setLayoutId(R.layout.double_container)
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment1)
@@ -1017,18 +1011,14 @@
}
}
- class ParentFragment : StrictViewFragment() {
- init {
- setLayoutId(R.layout.double_container)
- }
+ class ParentFragment : StrictViewFragment(R.layout.double_container) {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- val fragment2 = StrictViewFragment()
- fragment2.setLayoutId(R.layout.fragment_a)
+ val fragment2 = StrictViewFragment(R.layout.fragment_a)
childFragmentManager.beginTransaction()
.add(R.id.fragmentContainer1, fragment2, "inner")
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
index 7c17551..4242a3b 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
@@ -64,7 +64,7 @@
FragmentTestUtil.executePendingTransactions(activityRule, fm)
- val weakActivity = WeakReference(LoaderActivity.sActivity)
+ val weakActivity = WeakReference(LoaderActivity.activity)
// Wait for everything to settle. We have to make sure that the old Activity
// is ready to be collected.
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
index 7000783..ec335ed 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
@@ -41,7 +41,7 @@
val activity = activityRule.activity
activityRule.runOnUiThread {
val parent = ParentFragment()
- parent.setRetainChildInstance(true)
+ parent.retainChildInstance = true
activity.supportFragmentManager.beginTransaction()
.add(parent, "parent")
@@ -54,7 +54,7 @@
var attachedTo: Context? = null
val latch = CountDownLatch(1)
- child.setOnAttachListener { context, _ ->
+ child.onAttachListener = { context ->
attachedTo = context
latch.countDown()
}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt
index 782fa5d..5432969 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt
+++ b/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt
@@ -20,7 +20,6 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.annotation.ContentView
import androidx.fragment.app.test.FragmentTestActivity
import androidx.fragment.test.R
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -116,7 +115,7 @@
val fm = activityRule.activity.supportFragmentManager
var startBlue = activityRule.activity.findViewById<View>(R.id.blueSquare)
- val fragment1 = TransitionFragment2()
+ val fragment1 = TransitionFragment(R.layout.scene2)
fm.beginTransaction()
.addSharedElement(startBlue, "blueSquare")
.replace(R.id.fragmentContainer, fragment1)
@@ -140,7 +139,7 @@
startBlue = activityRule.activity.findViewById(R.id.blueSquare)
- val fragment2 = TransitionFragment2()
+ val fragment2 = TransitionFragment(R.layout.scene2)
fm.beginTransaction()
.addSharedElement(startBlue, "blueSquare")
.replace(R.id.fragmentContainer, fragment2)
@@ -906,8 +905,7 @@
assertThat(fragment.sharedElementReturn.getTargets().isEmpty()).isTrue()
}
- @ContentView(R.layout.scene1)
- open class PostponedFragment1 : TransitionFragment() {
+ open class PostponedFragment1 : TransitionFragment(R.layout.scene1) {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -917,8 +915,7 @@
}
}
- @ContentView(R.layout.scene2)
- class PostponedFragment2 : TransitionFragment() {
+ class PostponedFragment2 : TransitionFragment(R.layout.scene2) {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -937,7 +934,4 @@
.commitNow()
}
}
-
- @ContentView(R.layout.scene2)
- class TransitionFragment2 : TransitionFragment()
}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragment.java
index ae70d7f..8404459 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragment.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragment.java
@@ -37,7 +37,7 @@
public void onStateChanged(int fromState) {
super.onStateChanged(fromState);
// We execute the transaction when shutting down or after restoring
- if (fromState == mFromState && mState == mToState
+ if (fromState == mFromState && getCurrentState() == mToState
&& (mToState < mFromState || mIsRestored)) {
executeTransaction();
}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
new file mode 100644
index 0000000..304d0bf
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
@@ -0,0 +1,718 @@
+/*
+ * 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.fragment.app
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.FragmentTestUtil.HostCallbacks
+import androidx.fragment.app.FragmentTestUtil.createController
+import androidx.fragment.app.FragmentTestUtil.destroy
+import androidx.fragment.app.FragmentTestUtil.restartFragmentController
+import androidx.fragment.app.FragmentTestUtil.resume
+import androidx.fragment.app.FragmentTestUtil.shutdownFragmentController
+import androidx.fragment.app.FragmentTestUtil.startupFragmentController
+import androidx.fragment.app.test.EmptyFragmentTestActivity
+import androidx.fragment.test.R
+import androidx.lifecycle.ViewModelStore
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class SaveStateFragmentTest {
+
+ @get:Rule
+ val activityRule = ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ @Test
+ @UiThreadTest
+ fun setInitialSavedState() {
+ val fm = activityRule.activity.supportFragmentManager
+
+ // Add a StateSaveFragment
+ var fragment = StateSaveFragment("Saved", "")
+ fm.beginTransaction().add(fragment, "tag").commit()
+ executePendingTransactions(fm)
+
+ // Change the user visible hint before we save state
+ fragment.userVisibleHint = false
+
+ // Save its state and remove it
+ val state = fm.saveFragmentInstanceState(fragment)
+ fm.beginTransaction().remove(fragment).commit()
+ executePendingTransactions(fm)
+
+ // Create a new instance, calling setInitialSavedState
+ fragment = StateSaveFragment("", "")
+ fragment.setInitialSavedState(state)
+
+ // Add the new instance
+ fm.beginTransaction().add(fragment, "tag").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("setInitialSavedState did not restore saved state")
+ .that(fragment.savedState).isEqualTo("Saved")
+ assertWithMessage("setInitialSavedState did not restore user visible hint")
+ .that(fragment.userVisibleHint).isEqualTo(false)
+ }
+
+ @Test
+ @UiThreadTest
+ fun setInitialSavedStateWithSetUserVisibleHint() {
+ val fm = activityRule.activity.supportFragmentManager
+
+ // Add a StateSaveFragment
+ var fragment = StateSaveFragment("Saved", "")
+ fm.beginTransaction().add(fragment, "tag").commit()
+ executePendingTransactions(fm)
+
+ // Save its state and remove it
+ val state = fm.saveFragmentInstanceState(fragment)
+ fm.beginTransaction().remove(fragment).commit()
+ executePendingTransactions(fm)
+
+ // Create a new instance, calling setInitialSavedState
+ fragment = StateSaveFragment("", "")
+ fragment.setInitialSavedState(state)
+
+ // Change the user visible hint after we call setInitialSavedState
+ fragment.userVisibleHint = false
+
+ // Add the new instance
+ fm.beginTransaction().add(fragment, "tag").commit()
+ executePendingTransactions(fm)
+
+ assertWithMessage("setInitialSavedState did not restore saved state")
+ .that(fragment.savedState).isEqualTo("Saved")
+ assertWithMessage("setUserVisibleHint should override setInitialSavedState")
+ .that(fragment.userVisibleHint).isEqualTo(false)
+ }
+
+ @Test
+ @UiThreadTest
+ fun restoreRetainedInstanceFragments() {
+ // Create a new FragmentManager in isolation, nest some assorted fragments
+ // and then restore them to a second new FragmentManager.
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ // Configure fragments.
+
+ // This retained fragment will be added, then removed. After being removed, it
+ // should no longer be retained by the FragmentManager
+ val removedFragment = StateSaveFragment("Removed", "UnsavedRemoved")
+ removedFragment.retainInstance = true
+ fm1.beginTransaction().add(removedFragment, "tag:removed").commitNow()
+ fm1.beginTransaction().remove(removedFragment).commitNow()
+
+ // This retained fragment will be added, then detached. After being detached, it
+ // should continue to be retained by the FragmentManager
+ val detachedFragment = StateSaveFragment("Detached", "UnsavedDetached")
+ removedFragment.retainInstance = true
+ fm1.beginTransaction().add(detachedFragment, "tag:detached").commitNow()
+ fm1.beginTransaction().detach(detachedFragment).commitNow()
+
+ // Grandparent fragment will not retain instance
+ val grandparentFragment = StateSaveFragment("Grandparent", "UnsavedGrandparent")
+ assertWithMessage("grandparent fragment saved state not initialized")
+ .that(grandparentFragment.savedState).isNotNull()
+ assertWithMessage("grandparent fragment unsaved state not initialized")
+ .that(grandparentFragment.unsavedState).isNotNull()
+ fm1.beginTransaction().add(grandparentFragment, "tag:grandparent").commitNow()
+
+ // Parent fragment will retain instance
+ val parentFragment = StateSaveFragment("Parent", "UnsavedParent")
+ assertWithMessage("parent fragment saved state not initialized")
+ .that(parentFragment.savedState).isNotNull()
+ assertWithMessage("parent fragment unsaved state not initialized")
+ .that(parentFragment.unsavedState).isNotNull()
+ parentFragment.retainInstance = true
+ grandparentFragment.childFragmentManager.beginTransaction()
+ .add(parentFragment, "tag:parent").commitNow()
+ assertWithMessage("parent fragment is not a child of grandparent")
+ .that(parentFragment.parentFragment).isSameAs(grandparentFragment)
+
+ // Child fragment will not retain instance
+ val childFragment = StateSaveFragment("Child", "UnsavedChild")
+ assertWithMessage("child fragment saved state not initialized")
+ .that(childFragment.savedState).isNotNull()
+ assertWithMessage("child fragment unsaved state not initialized")
+ .that(childFragment.unsavedState).isNotNull()
+ parentFragment.childFragmentManager.beginTransaction()
+ .add(childFragment, "tag:child").commitNow()
+ assertWithMessage("child fragment is not a child of grandparent")
+ .that(childFragment.parentFragment).isSameAs(parentFragment)
+
+ // Saved for comparison later
+ val parentChildFragmentManager = parentFragment.childFragmentManager
+
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Bring the state back down to destroyed, simulating an activity restart
+ fc1.dispatchPause()
+ val savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+
+ // Create the new controller and restore state
+ val fc2 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm2 = fc2.supportFragmentManager
+
+ fc2.attachHost(null)
+ fc2.restoreSaveState(savedState)
+ fc2.dispatchCreate()
+
+ // Confirm that the restored fragments are available and in the expected states
+ val restoredRemovedFragment = fm2.findFragmentByTag("tag:removed") as StateSaveFragment?
+ assertThat(restoredRemovedFragment).isNull()
+ assertWithMessage("Removed Fragment should be destroyed")
+ .that(removedFragment.calledOnDestroy).isTrue()
+
+ val restoredDetachedFragment = fm2.findFragmentByTag("tag:detached") as StateSaveFragment
+ assertThat(restoredDetachedFragment).isNotNull()
+
+ val restoredGrandparent = fm2.findFragmentByTag("tag:grandparent") as StateSaveFragment
+ assertWithMessage("grandparent fragment not restored").that(restoredGrandparent).isNotNull()
+
+ assertWithMessage("grandparent fragment instance was saved")
+ .that(restoredGrandparent).isNotSameAs(grandparentFragment)
+ assertWithMessage("grandparent fragment saved state was not equal")
+ .that(restoredGrandparent.savedState).isEqualTo(grandparentFragment.savedState)
+ assertWithMessage("grandparent fragment unsaved state was unexpectedly preserved")
+ .that(restoredGrandparent.unsavedState).isNotEqualTo(grandparentFragment.unsavedState)
+
+ val restoredParent = restoredGrandparent
+ .childFragmentManager.findFragmentByTag("tag:parent") as StateSaveFragment
+ assertWithMessage("parent fragment not restored").that(restoredParent).isNotNull()
+
+ assertWithMessage("parent fragment instance was not saved")
+ .that(restoredParent).isSameAs(parentFragment)
+ assertWithMessage("parent fragment saved state was not equal")
+ .that(restoredParent.savedState).isEqualTo(parentFragment.savedState)
+ assertWithMessage("parent fragment unsaved state was not equal")
+ .that(restoredParent.unsavedState).isEqualTo(parentFragment.unsavedState)
+ assertWithMessage("parent fragment has the same child FragmentManager")
+ .that(restoredParent.childFragmentManager).isNotSameAs(parentChildFragmentManager)
+
+ val restoredChild = restoredParent
+ .childFragmentManager.findFragmentByTag("tag:child") as StateSaveFragment
+ assertWithMessage("child fragment not restored").that(restoredChild).isNotNull()
+
+ assertWithMessage("child fragment instance state was saved")
+ .that(restoredChild).isNotSameAs(childFragment)
+ assertWithMessage("child fragment saved state was not equal")
+ .that(restoredChild.savedState).isEqualTo(childFragment.savedState)
+ assertWithMessage("child fragment saved state was unexpectedly equal")
+ .that(restoredChild.unsavedState).isNotEqualTo(childFragment.unsavedState)
+
+ fc2.dispatchActivityCreated()
+ fc2.noteStateNotSaved()
+ fc2.execPendingActions()
+ fc2.dispatchStart()
+ fc2.dispatchResume()
+ fc2.execPendingActions()
+
+ // Test that the fragments are in the configuration we expect
+
+ // Bring the state back down to destroyed before we finish the test
+ shutdownFragmentController(fc2, viewModelStore)
+
+ assertWithMessage("grandparent not destroyed")
+ .that(restoredGrandparent.calledOnDestroy).isTrue()
+ assertWithMessage("parent not destroyed").that(restoredParent.calledOnDestroy).isTrue()
+ assertWithMessage("child not destroyed").that(restoredChild.calledOnDestroy).isTrue()
+ }
+
+ @Test
+ @UiThreadTest
+ fun restoreRetainedInstanceFragmentWithTransparentActivityConfigChange() {
+ // Create a new FragmentManager in isolation, add a retained instance Fragment,
+ // then mimic the following scenario:
+ // 1. Activity A adds retained Fragment F
+ // 2. Activity A starts translucent Activity B
+ // 3. Activity B start opaque Activity C
+ // 4. Rotate phone
+ // 5. Finish Activity C
+ // 6. Finish Activity B
+
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ // Add the retained Fragment
+ val retainedFragment = StateSaveFragment("Retained", "UnsavedRetained")
+ retainedFragment.retainInstance = true
+ fm1.beginTransaction().add(retainedFragment, "tag:retained").commitNow()
+
+ // Move the activity to resumed
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Launch the transparent activity on top
+ fc1.dispatchPause()
+
+ // Launch the opaque activity on top
+ val savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+
+ // Finish the opaque activity, making our Activity visible i.e., started
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+
+ // Finish the transparent activity, causing a config change
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+
+ // Create the new controller and restore state
+ val fc2 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm2 = fc2.supportFragmentManager
+
+ fc2.attachHost(null)
+ fc2.restoreSaveState(savedState)
+ fc2.dispatchCreate()
+
+ val restoredFragment = fm2.findFragmentByTag("tag:retained") as StateSaveFragment
+ assertWithMessage("retained fragment not restored").that(restoredFragment).isNotNull()
+ assertWithMessage("The retained Fragment shouldn't be recreated")
+ .that(restoredFragment).isEqualTo(retainedFragment)
+
+ fc2.dispatchActivityCreated()
+ fc2.noteStateNotSaved()
+ fc2.execPendingActions()
+ fc2.dispatchStart()
+ fc2.dispatchResume()
+ fc2.execPendingActions()
+
+ // Bring the state back down to destroyed before we finish the test
+ shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun testSavedInstanceStateAfterRestore() {
+
+ val viewModelStore = ViewModelStore()
+ val fc1 =
+ startupFragmentController(activityRule.activity, null, viewModelStore)
+ val fm1 = fc1.supportFragmentManager
+
+ // Add the initial state
+ val parentFragment = StrictFragment()
+ parentFragment.retainInstance = true
+ val childFragment = StrictFragment()
+ fm1.beginTransaction().add(parentFragment, "parent").commitNow()
+ val childFragmentManager = parentFragment.childFragmentManager
+ childFragmentManager.beginTransaction().add(childFragment, "child").commitNow()
+
+ // Confirm the initial state
+ assertWithMessage("Initial parent saved instance state should be null")
+ .that(parentFragment.lastSavedInstanceState).isNull()
+ assertWithMessage("Initial child saved instance state should be null")
+ .that(childFragment.lastSavedInstanceState).isNull()
+
+ // Bring the state back down to destroyed, simulating an activity restart
+ fc1.dispatchPause()
+ val savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+
+ // Create the new controller and restore state
+ val fc2 = startupFragmentController(
+ activityRule.activity,
+ savedState,
+ viewModelStore
+ )
+ val fm2 = fc2.supportFragmentManager
+
+ val restoredParentFragment = fm2.findFragmentByTag("parent") as StrictFragment
+ assertWithMessage("Parent fragment was not restored")
+ .that(restoredParentFragment).isNotNull()
+ val restoredChildFragment = restoredParentFragment
+ .childFragmentManager.findFragmentByTag("child") as StrictFragment
+ assertWithMessage("Child fragment was not restored").that(restoredChildFragment).isNotNull()
+
+ assertWithMessage("Parent fragment saved instance state should still be null since it is " +
+ "a retained Fragment").that(restoredParentFragment.lastSavedInstanceState).isNull()
+ assertWithMessage("Child fragment saved instance state should be non-null")
+ .that(restoredChildFragment.lastSavedInstanceState).isNotNull()
+
+ // Bring the state back down to destroyed before we finish the test
+ shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun restoreNestedFragmentsOnBackStack() {
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ // Add the initial state
+ val parentFragment = StrictFragment()
+ val childFragment = StrictFragment()
+ fm1.beginTransaction().add(parentFragment, "parent").commitNow()
+ val childFragmentManager = parentFragment.childFragmentManager
+ childFragmentManager.beginTransaction().add(childFragment, "child").commitNow()
+
+ // Now add a Fragment to the back stack
+ val replacementChildFragment = StrictFragment()
+ childFragmentManager.beginTransaction()
+ .remove(childFragment)
+ .add(replacementChildFragment, "child")
+ .addToBackStack("back_stack").commit()
+ childFragmentManager.executePendingTransactions()
+
+ // Move the activity to resumed
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Now bring the state back down
+ fc1.dispatchPause()
+ val savedState = fc1.saveAllState()
+ fc1.dispatchStop()
+ fc1.dispatchDestroy()
+
+ // Create the new controller and restore state
+ val fc2 = FragmentController.createController(
+ HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm2 = fc2.supportFragmentManager
+
+ fc2.attachHost(null)
+ fc2.restoreSaveState(savedState)
+ fc2.dispatchCreate()
+
+ val restoredParentFragment = fm2.findFragmentByTag("parent") as StrictFragment
+ assertWithMessage("Parent fragment was not restored")
+ .that(restoredParentFragment).isNotNull()
+ val restoredChildFragment = restoredParentFragment
+ .childFragmentManager.findFragmentByTag("child") as StrictFragment
+ assertWithMessage("Child fragment was not restored").that(restoredChildFragment).isNotNull()
+
+ fc2.dispatchActivityCreated()
+ fc2.noteStateNotSaved()
+ fc2.execPendingActions()
+ fc2.dispatchStart()
+ fc2.dispatchResume()
+ fc2.execPendingActions()
+
+ // Bring the state back down to destroyed before we finish the test
+ shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ /**
+ * When a fragment has been optimized out, it state should still be saved during
+ * save and restore instance state.
+ */
+ @Test
+ @UiThreadTest
+ fun saveRemovedFragment() {
+ var fc = createController(activityRule)
+ resume(activityRule, fc, null)
+ var fm = fc.supportFragmentManager
+
+ var fragment1 = SaveStateFragment.create(1)
+ fm.beginTransaction()
+ .add(android.R.id.content, fragment1, "1")
+ .addToBackStack(null)
+ .commit()
+ var fragment2 = SaveStateFragment.create(2)
+ fm.beginTransaction()
+ .replace(android.R.id.content, fragment2, "2")
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+
+ val savedState = destroy(activityRule, fc)
+
+ fc = createController(activityRule)
+ resume(activityRule, fc, savedState)
+ fm = fc.supportFragmentManager
+ fragment2 = fm.findFragmentByTag("2") as SaveStateFragment
+ assertThat(fragment2).isNotNull()
+ assertThat(fragment2.value).isEqualTo(2)
+ fm.popBackStackImmediate()
+ fragment1 = fm.findFragmentByTag("1") as SaveStateFragment
+ assertThat(fragment1).isNotNull()
+ assertThat(fragment1.value).isEqualTo(1)
+ }
+
+ /**
+ * Test to ensure that when dispatch* is called that the fragment manager
+ * doesn't cause the contained fragment states to change even if no state changes.
+ */
+ @Test
+ @UiThreadTest
+ fun noPrematureStateChange() {
+ val viewModelStore = ViewModelStore()
+ var fc =
+ startupFragmentController(activityRule.activity, null, viewModelStore)
+ var fm = fc.supportFragmentManager
+
+ fm.beginTransaction().add(StrictFragment(), "1").commitNow()
+
+ fc = restartFragmentController(activityRule.activity, fc, viewModelStore)
+
+ fm = fc.supportFragmentManager
+
+ val fragment1 = fm.findFragmentByTag("1") as StrictFragment
+ assertWithMessage("Fragment should be resumed after restart")
+ .that(fragment1.calledOnResume).isTrue()
+ fragment1.calledOnResume = false
+ fc.dispatchResume()
+
+ assertWithMessage("Fragment should not get onResume() after second dispatchResume()")
+ .that(fragment1.calledOnResume).isFalse()
+ }
+
+ @Test
+ @UiThreadTest
+ fun testIsStateSaved() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ startupFragmentController(activityRule.activity, null, viewModelStore)
+ val fm = fc.supportFragmentManager
+
+ val f = StrictFragment()
+ fm.beginTransaction().add(f, "1").commitNow()
+
+ assertWithMessage("fragment reported state saved while resumed")
+ .that(f.isStateSaved).isFalse()
+
+ fc.dispatchPause()
+ fc.saveAllState()
+
+ assertWithMessage("fragment reported state not saved after saveAllState")
+ .that(f.isStateSaved).isTrue()
+
+ fc.dispatchStop()
+
+ assertWithMessage("fragment reported state not saved after stop")
+ .that(f.isStateSaved).isTrue()
+
+ viewModelStore.clear()
+ fc.dispatchDestroy()
+
+ assertWithMessage("fragment reported state saved after destroy")
+ .that(f.isStateSaved).isFalse()
+ }
+
+ @Test
+ @UiThreadTest
+ fun saveAnimationState() {
+ val viewModelStore = ViewModelStore()
+ var fc = startupFragmentController(
+ activityRule.activity, null,
+ viewModelStore
+ )
+ var fm = fc.supportFragmentManager
+
+ fm.beginTransaction()
+ .setCustomAnimations(0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+ .add(android.R.id.content, SimpleFragment.create(R.layout.fragment_a))
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+
+ assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+
+ // Causes save and restore of fragments and back stack
+ fc = restartFragmentController(activityRule.activity, fc, viewModelStore)
+ fm = fc.supportFragmentManager
+
+ assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+
+ fm.beginTransaction()
+ .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, 0, 0)
+ .replace(android.R.id.content, SimpleFragment.create(R.layout.fragment_b))
+ .addToBackStack(null)
+ .commit()
+ fm.executePendingTransactions()
+
+ assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0)
+
+ // Causes save and restore of fragments and back stack
+ fc = restartFragmentController(activityRule.activity, fc, viewModelStore)
+ fm = fc.supportFragmentManager
+
+ assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0)
+
+ fm.popBackStackImmediate()
+
+ assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+
+ shutdownFragmentController(fc, viewModelStore)
+ }
+
+ private fun executePendingTransactions(fm: FragmentManager) {
+ activityRule.runOnUiThread { fm.executePendingTransactions() }
+ }
+
+ private fun assertAnimationsMatch(
+ fm: FragmentManager,
+ enter: Int,
+ exit: Int,
+ popEnter: Int,
+ popExit: Int
+ ) {
+ val fmImpl = fm as FragmentManagerImpl
+ val record = fmImpl.mBackStack[fmImpl.mBackStack.size - 1]
+
+ assertThat(record.mEnterAnim).isEqualTo(enter)
+ assertThat(record.mExitAnim).isEqualTo(exit)
+ assertThat(record.mPopEnterAnim).isEqualTo(popEnter)
+ assertThat(record.mPopExitAnim).isEqualTo(popExit)
+ }
+
+ class SimpleFragment : Fragment() {
+ private var layoutId: Int = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState != null) {
+ layoutId = savedInstanceState.getInt(LAYOUT_ID, layoutId)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt(LAYOUT_ID, layoutId)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = inflater.inflate(layoutId, container, false)
+
+ companion object {
+ private const val LAYOUT_ID = "layoutId"
+
+ fun create(layoutId: Int): SimpleFragment {
+ val fragment = SimpleFragment()
+ fragment.layoutId = layoutId
+ return fragment
+ }
+ }
+ }
+
+ class SaveStateFragment : Fragment() {
+ var value: Int = 0
+ private set
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt(VALUE_KEY, value)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState != null) {
+ value = savedInstanceState.getInt(VALUE_KEY, value)
+ }
+ }
+
+ companion object {
+ private const val VALUE_KEY = "SaveStateFragment.mValue"
+
+ fun create(value: Int): SaveStateFragment {
+ val saveStateFragment = SaveStateFragment()
+ saveStateFragment.value = value
+ return saveStateFragment
+ }
+ }
+ }
+
+ class StateSaveFragment : StrictFragment {
+
+ var savedState: String? = null
+ private set
+ var unsavedState: String? = null
+
+ constructor()
+
+ constructor(savedState: String, unsavedState: String) {
+ this.savedState = savedState
+ this.unsavedState = unsavedState
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState != null) {
+ savedState = savedInstanceState.getString(STATE_KEY)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString(STATE_KEY, savedState)
+ }
+
+ companion object {
+ private const val STATE_KEY = "state"
+ }
+ }
+}
\ No newline at end of file
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.java
deleted file mode 100644
index 2acbeda..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app;
-
-import android.content.Context;
-import android.os.Bundle;
-
-/**
- * This fragment watches its primary lifecycle events and throws IllegalStateException
- * if any of them are called out of order or from a bad/unexpected state.
- */
-public class StrictFragment extends Fragment {
- public static final int DETACHED = 0;
- public static final int ATTACHED = 1;
- public static final int CREATED = 2;
- public static final int ACTIVITY_CREATED = 3;
- public static final int STARTED = 4;
- public static final int RESUMED = 5;
-
- int mState;
-
- boolean mCalledOnAttach, mCalledOnCreate, mCalledOnActivityCreated,
- mCalledOnStart, mCalledOnResume, mCalledOnSaveInstanceState,
- mCalledOnPause, mCalledOnStop, mCalledOnDestroy, mCalledOnDetach,
- mCalledOnAttachFragment;
- Bundle mSavedInstanceState;
-
- static String stateToString(int state) {
- switch (state) {
- case DETACHED: return "DETACHED";
- case ATTACHED: return "ATTACHED";
- case CREATED: return "CREATED";
- case ACTIVITY_CREATED: return "ACTIVITY_CREATED";
- case STARTED: return "STARTED";
- case RESUMED: return "RESUMED";
- }
- return "(unknown " + state + ")";
- }
-
- public void onStateChanged(int fromState) {
- checkGetActivity();
- }
-
- public void checkGetActivity() {
- if (getActivity() == null) {
- throw new IllegalStateException("getActivity() returned null at unexpected time");
- }
- }
-
- public void checkState(String caller, int... expected) {
- if (expected == null || expected.length == 0) {
- throw new IllegalArgumentException("must supply at least one expected state");
- }
- for (int expect : expected) {
- if (mState == expect) {
- return;
- }
- }
- final StringBuilder expectString = new StringBuilder(stateToString(expected[0]));
- for (int i = 1; i < expected.length; i++) {
- expectString.append(" or ").append(stateToString(expected[i]));
- }
- throw new IllegalStateException(caller + " called while fragment was "
- + stateToString(mState) + "; expected " + expectString.toString());
- }
-
- public void checkStateAtLeast(String caller, int minState) {
- if (mState < minState) {
- throw new IllegalStateException(caller + " called while fragment was "
- + stateToString(mState) + "; expected at least " + stateToString(minState));
- }
- }
-
- @Override
- public void onAttachFragment(Fragment childFragment) {
- mCalledOnAttachFragment = true;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mCalledOnAttach = true;
- checkState("onAttach", DETACHED);
- mState = ATTACHED;
- onStateChanged(DETACHED);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (mCalledOnCreate && !mCalledOnDestroy) {
- throw new IllegalStateException("onCreate called more than once with no onDestroy");
- }
- mCalledOnCreate = true;
- mSavedInstanceState = savedInstanceState;
- checkState("onCreate", ATTACHED);
- mState = CREATED;
- onStateChanged(ATTACHED);
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- mCalledOnActivityCreated = true;
- checkState("onActivityCreated", ATTACHED, CREATED);
- int fromState = mState;
- mState = ACTIVITY_CREATED;
- onStateChanged(fromState);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mCalledOnStart = true;
- checkState("onStart", CREATED, ACTIVITY_CREATED);
- mState = STARTED;
- onStateChanged(ACTIVITY_CREATED);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- mCalledOnResume = true;
- checkState("onResume", STARTED);
- mState = RESUMED;
- onStateChanged(STARTED);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mCalledOnSaveInstanceState = true;
- checkGetActivity();
- // FIXME: We should not allow onSaveInstanceState except when STARTED or greater.
- // But FragmentManager currently does it in saveAllState for fragments on the
- // back stack, so fragments may be in the CREATED state.
- checkStateAtLeast("onSaveInstanceState", CREATED);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- mCalledOnPause = true;
- checkState("onPause", RESUMED);
- mState = STARTED;
- onStateChanged(RESUMED);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- mCalledOnStop = true;
- checkState("onStop", STARTED);
- mState = CREATED;
- onStateChanged(STARTED);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mCalledOnDestroy = true;
- checkState("onDestroy", CREATED);
- mState = ATTACHED;
- onStateChanged(CREATED);
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- mCalledOnDetach = true;
- checkState("onDestroy", CREATED, ATTACHED);
- int fromState = mState;
- mState = DETACHED;
- onStateChanged(fromState);
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.kt b/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.kt
new file mode 100644
index 0000000..b8faca2
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/StrictFragment.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright 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 androidx.fragment.app
+
+import android.content.Context
+import android.os.Bundle
+import com.google.common.truth.Truth.assertWithMessage
+
+/**
+ * This fragment watches its primary lifecycle events and throws IllegalStateException
+ * if any of them are called out of order or from a bad/unexpected state.
+ */
+open class StrictFragment : Fragment() {
+ var currentState: Int = 0
+
+ var calledOnAttach: Boolean = false
+ var calledOnCreate: Boolean = false
+ var calledOnActivityCreated: Boolean = false
+ var calledOnStart: Boolean = false
+ var calledOnResume: Boolean = false
+ var calledOnSaveInstanceState: Boolean = false
+ var calledOnPause: Boolean = false
+ var calledOnStop: Boolean = false
+ var calledOnDestroy: Boolean = false
+ var calledOnDetach: Boolean = false
+ var calledOnAttachFragment: Boolean = false
+ var lastSavedInstanceState: Bundle? = null
+
+ open fun onStateChanged(fromState: Int) {
+ checkGetActivity()
+ }
+
+ fun checkGetActivity() {
+ assertWithMessage("getActivity() returned null at unexpected time")
+ .that(activity)
+ .isNotNull()
+ }
+
+ fun checkState(caller: String, vararg expected: Int) {
+ if (expected.isEmpty()) {
+ throw IllegalArgumentException("must supply at least one expected state")
+ }
+ for (expect in expected) {
+ if (currentState == expect) {
+ return
+ }
+ }
+ val expectString = StringBuilder(stateToString(expected[0]))
+ for (i in 1 until expected.size) {
+ expectString.append(" or ").append(stateToString(expected[i]))
+ }
+ throw IllegalStateException(
+ "$caller called while fragment was ${stateToString(currentState)}; " +
+ "expected $expectString"
+ )
+ }
+
+ fun checkStateAtLeast(caller: String, minState: Int) {
+ if (currentState < minState) {
+ throw IllegalStateException(
+ "$caller called while fragment was ${stateToString(currentState)}; " +
+ "expected at least ${stateToString(minState)}"
+ )
+ }
+ }
+
+ override fun onAttachFragment(childFragment: Fragment) {
+ calledOnAttachFragment = true
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ calledOnAttach = true
+ checkState("onAttach", DETACHED)
+ currentState = ATTACHED
+ onStateChanged(DETACHED)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (calledOnCreate && !calledOnDestroy) {
+ throw IllegalStateException("onCreate called more than once with no onDestroy")
+ }
+ calledOnCreate = true
+ lastSavedInstanceState = savedInstanceState
+ checkState("onCreate", ATTACHED)
+ currentState = CREATED
+ onStateChanged(ATTACHED)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ calledOnActivityCreated = true
+ checkState("onActivityCreated", ATTACHED, CREATED)
+ val fromState = currentState
+ currentState = ACTIVITY_CREATED
+ onStateChanged(fromState)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ calledOnStart = true
+ checkState("onStart", CREATED, ACTIVITY_CREATED)
+ currentState = STARTED
+ onStateChanged(ACTIVITY_CREATED)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ calledOnResume = true
+ checkState("onResume", STARTED)
+ currentState = RESUMED
+ onStateChanged(STARTED)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ calledOnSaveInstanceState = true
+ checkGetActivity()
+ // FIXME: We should not allow onSaveInstanceState except when STARTED or greater.
+ // But FragmentManager currently does it in saveAllState for fragments on the
+ // back stack, so fragments may be in the CREATED state.
+ checkStateAtLeast("onSaveInstanceState", CREATED)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ calledOnPause = true
+ checkState("onPause", RESUMED)
+ currentState = STARTED
+ onStateChanged(RESUMED)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ calledOnStop = true
+ checkState("onStop", STARTED)
+ currentState = CREATED
+ onStateChanged(STARTED)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ calledOnDestroy = true
+ checkState("onDestroy", CREATED)
+ currentState = ATTACHED
+ onStateChanged(CREATED)
+ }
+
+ override fun onDetach() {
+ super.onDetach()
+ calledOnDetach = true
+ checkState("onDestroy", CREATED, ATTACHED)
+ val fromState = currentState
+ currentState = DETACHED
+ onStateChanged(fromState)
+ }
+
+ companion object {
+ const val DETACHED = 0
+ const val ATTACHED = 1
+ const val CREATED = 2
+ const val ACTIVITY_CREATED = 3
+ const val STARTED = 4
+ const val RESUMED = 5
+
+ internal fun stateToString(state: Int): String {
+ when (state) {
+ DETACHED -> return "DETACHED"
+ ATTACHED -> return "ATTACHED"
+ CREATED -> return "CREATED"
+ ACTIVITY_CREATED -> return "ACTIVITY_CREATED"
+ STARTED -> return "STARTED"
+ RESUMED -> return "RESUMED"
+ }
+ return "(unknown $state)"
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.java
deleted file mode 100644
index ec1a553..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.test.R;
-
-public class StrictViewFragment extends StrictFragment {
- boolean mOnCreateViewCalled, mOnViewCreatedCalled, mOnDestroyViewCalled;
- int mLayoutId = R.layout.strict_view_fragment;
-
- public void setLayoutId(int layoutId) {
- mLayoutId = layoutId;
- }
-
- public static StrictViewFragment create(int layoutId) {
- StrictViewFragment fragment = new StrictViewFragment();
- fragment.mLayoutId = layoutId;
- return fragment;
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- checkGetActivity();
- checkState("onCreateView", CREATED);
- View result = super.onCreateView(inflater, container, savedInstanceState);
- if (result == null) {
- result = inflater.inflate(mLayoutId, container, false);
- }
- mOnCreateViewCalled = true;
- return result;
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- if (view == null) {
- throw new IllegalArgumentException("onViewCreated view argument should not be null");
- }
- checkGetActivity();
- checkState("onViewCreated", CREATED);
- mOnViewCreatedCalled = true;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- if (getView() == null) {
- throw new IllegalStateException("getView returned null in onDestroyView");
- }
- checkGetActivity();
- checkState("onDestroyView", CREATED);
- mOnDestroyViewCalled = true;
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt b/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
new file mode 100644
index 0000000..5b367e4
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/StrictViewFragment.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 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 androidx.fragment.app
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.fragment.test.R
+import com.google.common.truth.Truth.assertWithMessage
+
+open class StrictViewFragment(
+ @LayoutRes val contentLayoutId: Int = R.layout.strict_view_fragment
+) : StrictFragment() {
+
+ internal var onCreateViewCalled: Boolean = false
+ internal var onViewCreatedCalled: Boolean = false
+ internal var onDestroyViewCalled: Boolean = false
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ checkGetActivity()
+ checkState("onCreateView", StrictFragment.CREATED)
+ var result = super.onCreateView(inflater, container, savedInstanceState)
+ if (result == null) {
+ result = inflater.inflate(contentLayoutId, container, false)
+ }
+ onCreateViewCalled = true
+ return result
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ checkGetActivity()
+ checkState("onViewCreated", StrictFragment.CREATED)
+ onViewCreatedCalled = true
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ assertWithMessage("getView returned null in onDestroyView")
+ .that(view)
+ .isNotNull()
+ checkGetActivity()
+ checkState("onDestroyView", StrictFragment.CREATED)
+ onDestroyViewCalled = true
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt b/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
new file mode 100644
index 0000000..0a62f5e
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
@@ -0,0 +1,427 @@
+/*
+ * 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.fragment.app
+
+import android.os.Bundle
+import androidx.fragment.app.test.EmptyFragmentTestActivity
+import androidx.lifecycle.ViewModelStore
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class TargetFragmentLifeCycleTest {
+
+ @get:Rule
+ val activityRule = ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ @Test
+ fun targetFragmentNoCycles() {
+ val one = Fragment()
+ val two = Fragment()
+ val three = Fragment()
+
+ try {
+ one.setTargetFragment(two, 0)
+ two.setTargetFragment(three, 0)
+ three.setTargetFragment(one, 0)
+ Assert.fail("creating a fragment target cycle did not throw IllegalArgumentException")
+ } catch (e: IllegalArgumentException) {
+ assertThat(e).hasMessageThat().contains("Setting $one as the target of $three would" +
+ " create a target cycle")
+ }
+ }
+
+ @Test
+ fun targetFragmentSetClear() {
+ val one = Fragment()
+ val two = Fragment()
+
+ one.setTargetFragment(two, 0)
+ one.setTargetFragment(null, 0)
+ }
+
+ /**
+ * Test that target fragments are in a useful state when we restore them, even if they're
+ * on the back stack.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentRestoreLifecycleStateBackStack() {
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ FragmentTestUtil.HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ val target = TargetFragment()
+ fm1.beginTransaction().add(target, "target").commitNow()
+
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ fm1.beginTransaction()
+ .remove(target)
+ .add(referrer, "referrer")
+ .addToBackStack(null)
+ .commit()
+
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Simulate an activity restart
+ val fc2 =
+ FragmentTestUtil.restartFragmentController(activityRule.activity, fc1, viewModelStore)
+
+ // Bring the state back down to destroyed before we finish the test
+ FragmentTestUtil.shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun targetFragmentRestoreLifecycleStateManagerOrder() {
+ val viewModelStore = ViewModelStore()
+ val fc1 = FragmentController.createController(
+ FragmentTestUtil.HostCallbacks(activityRule.activity, viewModelStore)
+ )
+
+ val fm1 = fc1.supportFragmentManager
+
+ fc1.attachHost(null)
+ fc1.dispatchCreate()
+
+ val target1 = TargetFragment()
+ val referrer1 = ReferrerFragment()
+ referrer1.setTargetFragment(target1, 0)
+
+ fm1.beginTransaction().add(target1, "target1").add(referrer1, "referrer1").commitNow()
+
+ val target2 = TargetFragment()
+ val referrer2 = ReferrerFragment()
+ referrer2.setTargetFragment(target2, 0)
+
+ // Order shouldn't matter.
+ fm1.beginTransaction().add(referrer2, "referrer2").add(target2, "target2").commitNow()
+
+ fc1.dispatchActivityCreated()
+ fc1.noteStateNotSaved()
+ fc1.execPendingActions()
+ fc1.dispatchStart()
+ fc1.dispatchResume()
+ fc1.execPendingActions()
+
+ // Simulate an activity restart
+ val fc2 =
+ FragmentTestUtil.restartFragmentController(activityRule.activity, fc1, viewModelStore)
+
+ // Bring the state back down to destroyed before we finish the test
+ FragmentTestUtil.shutdownFragmentController(fc2, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun targetFragmentClearedWhenSetToNull() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ referrer.setTargetFragment(null, 0)
+
+ assertWithMessage("Target Fragment should cleared after setTargetFragment with null")
+ .that(referrer.targetFragment).isNull()
+
+ fm.beginTransaction().remove(referrer).commitNow()
+
+ assertWithMessage("Target Fragment should still be cleared after being removed")
+ .that(referrer.targetFragment).isNull()
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+ }
+
+ @Test
+ @UiThreadTest
+ fun targetFragment_replacement() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val referrer = ReferrerFragment()
+ val target = TargetFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(referrer, "referrer").add(target, "target").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ val newTarget = TargetFragment()
+ referrer.setTargetFragment(newTarget, 0)
+
+ assertWithMessage("New Target Fragment should returned despite not being added")
+ .that(referrer.targetFragment).isSameAs(newTarget)
+
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Replacement Target Fragment should override previous target")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target Fragment is already
+ * attached to a FragmentManager, but the referrer Fragment is not attached.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentOnlyTargetAdded() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ // Add just the target Fragment to the FragmentManager
+ fm.beginTransaction().add(target, "target").commitNow()
+
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().remove(referrer).commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being removed")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target fragment is
+ * not retained and the referrer fragment is not retained.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentNonRetainedNonRetained() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().remove(referrer).commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being removed")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+
+ assertWithMessage("Target Fragment should be accessible after destruction")
+ .that(referrer.targetFragment).isSameAs(target)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target fragment is
+ * retained and the referrer fragment is not retained.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentRetainedNonRetained() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ target.retainInstance = true
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().remove(referrer).commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being removed")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ FragmentTestUtil.shutdownFragmentController(fc, viewModelStore)
+
+ assertWithMessage("Target Fragment should be accessible after destruction")
+ .that(referrer.targetFragment).isSameAs(target)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target fragment is
+ * not retained and the referrer fragment is retained.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentNonRetainedRetained() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ val referrer = ReferrerFragment()
+ referrer.setTargetFragment(target, 0)
+ referrer.retainInstance = true
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ // Save the state
+ fc.dispatchPause()
+ fc.saveAllState()
+ fc.dispatchStop()
+ fc.dispatchDestroy()
+
+ assertWithMessage("Target Fragment should be accessible after target Fragment destruction")
+ .that(referrer.targetFragment).isSameAs(target)
+ }
+
+ /**
+ * Test the availability of getTargetFragment() when the target fragment is
+ * retained and the referrer fragment is also retained.
+ */
+ @Test
+ @UiThreadTest
+ fun targetFragmentRetainedRetained() {
+ val viewModelStore = ViewModelStore()
+ val fc =
+ FragmentTestUtil.startupFragmentController(activityRule.activity, null, viewModelStore)
+
+ val fm = fc.supportFragmentManager
+
+ val target = TargetFragment()
+ target.retainInstance = true
+ val referrer = ReferrerFragment()
+ referrer.retainInstance = true
+ referrer.setTargetFragment(target, 0)
+
+ assertWithMessage("Target Fragment should be accessible before being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ fm.beginTransaction().add(target, "target").add(referrer, "referrer").commitNow()
+
+ assertWithMessage("Target Fragment should be accessible after being added")
+ .that(referrer.targetFragment).isSameAs(target)
+
+ // Save the state
+ fc.dispatchPause()
+ fc.saveAllState()
+ fc.dispatchStop()
+ fc.dispatchDestroy()
+
+ assertWithMessage("Target Fragment should be accessible after FragmentManager destruction")
+ .that(referrer.targetFragment).isSameAs(target)
+ }
+
+ class TargetFragment : Fragment() {
+ var calledCreate: Boolean = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ calledCreate = true
+ }
+ }
+
+ class ReferrerFragment : Fragment() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val target = targetFragment
+ assertWithMessage("target fragment was null during referrer onCreate")
+ .that(target).isNotNull()
+
+ if (target !is TargetFragment) {
+ throw IllegalStateException("target fragment was not a TargetFragment")
+ }
+
+ assertWithMessage("target fragment has not yet been created")
+ .that(target.calledCreate).isTrue()
+ }
+ }
+}
\ No newline at end of file
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TrackingVisibility.java b/fragment/src/androidTest/java/androidx/fragment/app/TrackingVisibility.java
index 3b0dbb1a..2c54f2d 100644
--- a/fragment/src/androidTest/java/androidx/fragment/app/TrackingVisibility.java
+++ b/fragment/src/androidTest/java/androidx/fragment/app/TrackingVisibility.java
@@ -28,7 +28,7 @@
* Visibility transition that tracks which targets are applied to it.
* This transition does no animation.
*/
-class TrackingVisibility extends Visibility implements TargetTracking {
+public class TrackingVisibility extends Visibility implements TargetTracking {
public final ArrayList<View> targets = new ArrayList<>();
private final Rect[] mEpicenter = new Rect[1];
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.java b/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.java
deleted file mode 100644
index ad8b45b1..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app;
-
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-
-import android.os.Bundle;
-import android.os.SystemClock;
-import android.transition.Transition;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * A fragment that has transitions that can be tracked.
- */
-public class TransitionFragment extends StrictViewFragment {
- public final TrackingVisibility enterTransition = new TrackingVisibility();
- public final TrackingVisibility reenterTransition = new TrackingVisibility();
- public final TrackingVisibility exitTransition = new TrackingVisibility();
- public final TrackingVisibility returnTransition = new TrackingVisibility();
- public final TrackingTransition sharedElementEnter = new TrackingTransition();
- public final TrackingTransition sharedElementReturn = new TrackingTransition();
-
- private Transition.TransitionListener mListener = mock(Transition.TransitionListener.class);
-
- public TransitionFragment() {
- setEnterTransition(enterTransition);
- setReenterTransition(reenterTransition);
- setExitTransition(exitTransition);
- setReturnTransition(returnTransition);
- setSharedElementEnterTransition(sharedElementEnter);
- setSharedElementReturnTransition(sharedElementReturn);
- enterTransition.addListener(mListener);
- sharedElementEnter.addListener(mListener);
- reenterTransition.addListener(mListener);
- exitTransition.addListener(mListener);
- returnTransition.addListener(mListener);
- sharedElementReturn.addListener(mListener);
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- checkGetActivity();
- checkState("onCreateView", CREATED);
- mOnCreateViewCalled = true;
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- void waitForTransition() throws InterruptedException {
- verify(mListener, CtsMockitoUtils.within(1000)).onTransitionEnd((Transition) any());
- reset(mListener);
- }
-
- void waitForNoTransition() throws InterruptedException {
- SystemClock.sleep(250);
- verify(mListener, never()).onTransitionStart((Transition) any());
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.kt b/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.kt
new file mode 100644
index 0000000..bd20bd9
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/TransitionFragment.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 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 androidx.fragment.app
+
+import android.os.SystemClock
+import android.transition.Transition
+import androidx.annotation.LayoutRes
+import androidx.fragment.test.R
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+/**
+ * A fragment that has transitions that can be tracked.
+ */
+open class TransitionFragment(
+ @LayoutRes contentLayoutId: Int = R.layout.strict_view_fragment
+) : StrictViewFragment(contentLayoutId) {
+ val enterTransition = TrackingVisibility()
+ val reenterTransition = TrackingVisibility()
+ val exitTransition = TrackingVisibility()
+ val returnTransition = TrackingVisibility()
+ val sharedElementEnter = TrackingTransition()
+ val sharedElementReturn = TrackingTransition()
+
+ private val listener = mock(Transition.TransitionListener::class.java)
+
+ init {
+ @Suppress("LeakingThis")
+ setEnterTransition(enterTransition)
+ @Suppress("LeakingThis")
+ setReenterTransition(reenterTransition)
+ @Suppress("LeakingThis")
+ setExitTransition(exitTransition)
+ @Suppress("LeakingThis")
+ setReturnTransition(returnTransition)
+ sharedElementEnterTransition = sharedElementEnter
+ sharedElementReturnTransition = sharedElementReturn
+ enterTransition.addListener(listener)
+ sharedElementEnter.addListener(listener)
+ reenterTransition.addListener(listener)
+ exitTransition.addListener(listener)
+ returnTransition.addListener(listener)
+ sharedElementReturn.addListener(listener)
+ }
+
+ internal fun waitForTransition() {
+ verify(
+ listener,
+ CtsMockitoUtils.within(1000)
+ ).onTransitionEnd(ArgumentMatchers.any())
+ reset(listener)
+ }
+
+ internal fun waitForNoTransition() {
+ SystemClock.sleep(250)
+ verify(
+ listener,
+ never()
+ ).onTransitionStart(ArgumentMatchers.any())
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.java
deleted file mode 100644
index 0d7443d..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.java
+++ /dev/null
@@ -1,291 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app.test;
-
-import static org.junit.Assert.assertFalse;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.os.Bundle;
-import android.transition.Transition;
-import android.transition.Transition.TransitionListener;
-import android.transition.TransitionInflater;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.test.R;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A simple activity used for Fragment Transitions and lifecycle event ordering
- */
-@ContentView(R.layout.activity_content)
-public class FragmentTestActivity extends FragmentActivity {
- public final CountDownLatch onDestroyLatch = new CountDownLatch(1);
-
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- Intent intent = getIntent();
- if (intent != null && intent.getBooleanExtra("finishEarly", false)) {
- finish();
- getSupportFragmentManager().beginTransaction()
- .add(new AssertNotDestroyed(), "not destroyed")
- .commit();
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- onDestroyLatch.countDown();
- }
-
- public static class TestFragment extends Fragment {
- public static final int ENTER = 0;
- public static final int RETURN = 1;
- public static final int EXIT = 2;
- public static final int REENTER = 3;
- public static final int SHARED_ELEMENT_ENTER = 4;
- public static final int SHARED_ELEMENT_RETURN = 5;
- private static final int TRANSITION_COUNT = 6;
-
- private static final String LAYOUT_ID = "layoutId";
- private static final String TRANSITION_KEY = "transition_";
- private int mLayoutId = R.layout.fragment_start;
- private final int[] mTransitionIds = new int[] {
- R.transition.fade,
- R.transition.fade,
- R.transition.fade,
- R.transition.fade,
- R.transition.change_bounds,
- R.transition.change_bounds,
- };
- private final Object[] mListeners = new Object[TRANSITION_COUNT];
-
- public TestFragment() {
- if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- mListeners[i] = new TransitionCalledListener();
- }
- }
- }
-
- public static TestFragment create(int layoutId) {
- TestFragment testFragment = new TestFragment();
- testFragment.mLayoutId = layoutId;
- return testFragment;
- }
-
- public void clearTransitions() {
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- mTransitionIds[i] = 0;
- }
- }
-
- public void clearNotifications() {
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- ((TransitionCalledListener)mListeners[i]).startLatch = new CountDownLatch(1);
- ((TransitionCalledListener)mListeners[i]).endLatch = new CountDownLatch(1);
- }
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- mLayoutId = savedInstanceState.getInt(LAYOUT_ID, mLayoutId);
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- String key = TRANSITION_KEY + i;
- mTransitionIds[i] = savedInstanceState.getInt(key, mTransitionIds[i]);
- }
- }
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(LAYOUT_ID, mLayoutId);
- for (int i = 0; i < TRANSITION_COUNT; i++) {
- String key = TRANSITION_KEY + i;
- outState.putInt(key, mTransitionIds[i]);
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(mLayoutId, container, false);
- }
-
- @SuppressWarnings("deprecation")
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- if (VERSION.SDK_INT > VERSION_CODES.KITKAT) {
- setEnterTransition(loadTransition(ENTER));
- setReenterTransition(loadTransition(REENTER));
- setExitTransition(loadTransition(EXIT));
- setReturnTransition(loadTransition(RETURN));
- setSharedElementEnterTransition(loadTransition(SHARED_ELEMENT_ENTER));
- setSharedElementReturnTransition(loadTransition(SHARED_ELEMENT_RETURN));
- }
- }
-
- public boolean wasStartCalled(int transitionKey) {
- return ((TransitionCalledListener)mListeners[transitionKey]).startLatch.getCount() == 0;
- }
-
- public boolean wasEndCalled(int transitionKey) {
- return ((TransitionCalledListener)mListeners[transitionKey]).endLatch.getCount() == 0;
- }
-
- public boolean waitForStart(int transitionKey)
- throws InterruptedException {
- TransitionCalledListener l = ((TransitionCalledListener)mListeners[transitionKey]);
- return l.startLatch.await(500,TimeUnit.MILLISECONDS);
- }
-
- public boolean waitForEnd(int transitionKey)
- throws InterruptedException {
- TransitionCalledListener l = ((TransitionCalledListener)mListeners[transitionKey]);
- return l.endLatch.await(500,TimeUnit.MILLISECONDS);
- }
-
- private Transition loadTransition(int key) {
- final int id = mTransitionIds[key];
- if (id == 0) {
- return null;
- }
- Transition transition = TransitionInflater.from(getActivity()).inflateTransition(id);
- transition.addListener(((TransitionCalledListener)mListeners[key]));
- return transition;
- }
-
- private class TransitionCalledListener implements TransitionListener {
- public CountDownLatch startLatch = new CountDownLatch(1);
- public CountDownLatch endLatch = new CountDownLatch(1);
-
- public TransitionCalledListener() {
- }
-
- @Override
- public void onTransitionStart(Transition transition) {
- startLatch.countDown();
- }
-
- @Override
- public void onTransitionEnd(Transition transition) {
- endLatch.countDown();
- }
-
- @Override
- public void onTransitionCancel(Transition transition) {
- }
-
- @Override
- public void onTransitionPause(Transition transition) {
- }
-
- @Override
- public void onTransitionResume(Transition transition) {
- }
- }
- }
-
- public static class ParentFragment extends Fragment {
- static final String CHILD_FRAGMENT_TAG = "childFragment";
- public boolean wasAttachedInTime;
-
- private boolean mRetainChild;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- ChildFragment f = getChildFragment();
- if (f == null) {
- f = new ChildFragment();
- if (mRetainChild) {
- f.setRetainInstance(true);
- }
- getChildFragmentManager().beginTransaction().add(f, CHILD_FRAGMENT_TAG).commitNow();
- }
- wasAttachedInTime = f.attached;
- }
-
- public ChildFragment getChildFragment() {
- return (ChildFragment) getChildFragmentManager().findFragmentByTag(CHILD_FRAGMENT_TAG);
- }
-
- public void setRetainChildInstance(boolean retainChild) {
- mRetainChild = retainChild;
- }
- }
-
- public static class ChildFragment extends Fragment {
- private OnAttachListener mOnAttachListener;
-
- public boolean attached;
- public boolean onActivityResultCalled;
- public int onActivityResultRequestCode;
- public int onActivityResultResultCode;
-
- @Override
- public void onAttach(Context activity) {
- super.onAttach(activity);
- attached = true;
- if (mOnAttachListener != null) {
- mOnAttachListener.onAttach(activity, this);
- }
- }
-
- public void setOnAttachListener(OnAttachListener listener) {
- mOnAttachListener = listener;
- }
-
- public interface OnAttachListener {
- void onAttach(Context activity, ChildFragment fragment);
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- onActivityResultCalled = true;
- onActivityResultRequestCode = requestCode;
- onActivityResultResultCode = resultCode;
- }
- }
-
- public static class AssertNotDestroyed extends Fragment {
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
- assertFalse(getActivity().isDestroyed());
- }
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt b/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt
new file mode 100644
index 0000000..3ebf7c9
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/FragmentTestActivity.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 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 androidx.fragment.app.test
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build.VERSION
+import android.os.Build.VERSION_CODES
+import android.os.Bundle
+import androidx.annotation.ContentView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.test.R
+import org.junit.Assert.assertFalse
+import java.util.concurrent.CountDownLatch
+
+/**
+ * A simple activity used for Fragment Transitions and lifecycle event ordering
+ */
+@ContentView(R.layout.activity_content)
+class FragmentTestActivity : FragmentActivity() {
+ val onDestroyLatch = CountDownLatch(1)
+
+ public override fun onCreate(icicle: Bundle?) {
+ super.onCreate(icicle)
+ if (intent?.getBooleanExtra("finishEarly", false) == true) {
+ finish()
+ supportFragmentManager.beginTransaction()
+ .add(AssertNotDestroyed(), "not destroyed")
+ .commit()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ onDestroyLatch.countDown()
+ }
+
+ class ParentFragment : Fragment() {
+ var wasAttachedInTime: Boolean = false
+
+ var retainChildInstance: Boolean = false
+
+ val childFragment: ChildFragment
+ get() = childFragmentManager.findFragmentByTag(CHILD_FRAGMENT_TAG) as ChildFragment
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (childFragmentManager.findFragmentByTag(CHILD_FRAGMENT_TAG) == null) {
+ childFragmentManager.beginTransaction()
+ .add(ChildFragment().apply {
+ if (retainChildInstance) {
+ retainInstance = true
+ }
+ }, CHILD_FRAGMENT_TAG)
+ .commitNow()
+ }
+ wasAttachedInTime = childFragment.attached
+ }
+
+ companion object {
+ internal const val CHILD_FRAGMENT_TAG = "childFragment"
+ }
+ }
+
+ class ChildFragment : Fragment() {
+ var onAttachListener: (context: Context) -> Unit = {}
+
+ var attached: Boolean = false
+ var onActivityResultCalled: Boolean = false
+ var onActivityResultRequestCode: Int = 0
+ var onActivityResultResultCode: Int = 0
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ attached = true
+ onAttachListener.invoke(context)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ onActivityResultCalled = true
+ onActivityResultRequestCode = requestCode
+ onActivityResultResultCode = resultCode
+ }
+ }
+
+ class AssertNotDestroyed : Fragment() {
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+ assertFalse(requireActivity().isDestroyed)
+ }
+ }
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.java
deleted file mode 100644
index 971314b..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app.test;
-
-import android.os.Bundle;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.test.R;
-import androidx.testutils.RecreatedActivity;
-
-public class HangingFragmentActivity extends RecreatedActivity {
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(savedInstanceState == null ? R.layout.activity_inflated_fragment
- : R.layout.activity_content);
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.kt b/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.kt
new file mode 100644
index 0000000..39a4aff
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/HangingFragmentActivity.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 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 androidx.fragment.app.test
+
+import android.os.Bundle
+import androidx.fragment.test.R
+import androidx.testutils.RecreatedActivity
+
+class HangingFragmentActivity : RecreatedActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(
+ if (savedInstanceState == null)
+ R.layout.activity_inflated_fragment
+ else
+ R.layout.activity_content
+ )
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.java
deleted file mode 100644
index 77f4145..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app.test;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.ContentView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.test.R;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.AsyncTaskLoader;
-import androidx.loader.content.Loader;
-import androidx.testutils.RecreatedActivity;
-
-@ContentView(R.layout.activity_loader)
-public class LoaderActivity extends RecreatedActivity
- implements LoaderManager.LoaderCallbacks<String> {
- private static final int TEXT_LOADER_ID = 14;
-
- public TextView textView;
- public TextView textViewB;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- textView = findViewById(R.id.textA);
- textViewB = findViewById(R.id.textB);
-
- if (savedInstanceState == null) {
- getSupportFragmentManager()
- .beginTransaction()
- .add(R.id.fragmentContainer, new TextLoaderFragment())
- .commit();
- }
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- LoaderManager.getInstance(this).initLoader(TEXT_LOADER_ID, null, this);
- }
-
- @NonNull
- @Override
- public Loader<String> onCreateLoader(int id, @Nullable Bundle args) {
- return new TextLoader(this);
- }
-
- @Override
- public void onLoadFinished(@NonNull Loader<String> loader, String data) {
- textView.setText(data);
- }
-
- @Override
- public void onLoaderReset(@NonNull Loader<String> loader) {
- }
-
- static class TextLoader extends AsyncTaskLoader<String> {
- TextLoader(Context context) {
- super(context);
- }
-
- @Override
- protected void onStartLoading() {
- forceLoad();
- }
-
- @Override
- public String loadInBackground() {
- return "Loaded!";
- }
- }
-
- @ContentView(R.layout.fragment_c)
- public static class TextLoaderFragment extends Fragment
- implements LoaderManager.LoaderCallbacks<String> {
- public TextView textView;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- LoaderManager.getInstance(this).initLoader(TEXT_LOADER_ID, null, this);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- textView = view.findViewById(R.id.textC);
- }
-
- @NonNull
- @Override
- public Loader<String> onCreateLoader(int id, @Nullable Bundle args) {
- return new TextLoader(getContext());
- }
-
- @Override
- public void onLoadFinished(@NonNull Loader<String> loader, String data) {
- textView.setText(data);
- }
-
- @Override
- public void onLoaderReset(@NonNull Loader<String> loader) {
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.kt b/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.kt
new file mode 100644
index 0000000..ea44bf9
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/LoaderActivity.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 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 androidx.fragment.app.test
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.widget.TextView
+
+import androidx.annotation.ContentView
+import androidx.fragment.app.Fragment
+import androidx.fragment.test.R
+import androidx.loader.app.LoaderManager
+import androidx.loader.content.AsyncTaskLoader
+import androidx.loader.content.Loader
+import androidx.testutils.RecreatedActivity
+
+@ContentView(R.layout.activity_loader)
+class LoaderActivity : RecreatedActivity(), LoaderManager.LoaderCallbacks<String> {
+
+ lateinit var textView: TextView
+ lateinit var textViewB: TextView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ textView = findViewById(R.id.textA)
+ textViewB = findViewById(R.id.textB)
+
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .add(R.id.fragmentContainer, TextLoaderFragment())
+ .commit()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ LoaderManager.getInstance(this).initLoader(TEXT_LOADER_ID, null, this)
+ }
+
+ override fun onCreateLoader(id: Int, args: Bundle?): Loader<String> {
+ return TextLoader(this)
+ }
+
+ override fun onLoadFinished(loader: Loader<String>, data: String) {
+ textView.text = data
+ }
+
+ override fun onLoaderReset(loader: Loader<String>) {
+ }
+
+ internal class TextLoader(context: Context) : AsyncTaskLoader<String>(context) {
+
+ override fun onStartLoading() {
+ forceLoad()
+ }
+
+ override fun loadInBackground(): String? {
+ return "Loaded!"
+ }
+ }
+
+ @ContentView(R.layout.fragment_c)
+ class TextLoaderFragment : Fragment(), LoaderManager.LoaderCallbacks<String> {
+ lateinit var textView: TextView
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ LoaderManager.getInstance(this).initLoader(TEXT_LOADER_ID, null, this)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ textView = view.findViewById(R.id.textC)
+ }
+
+ override fun onCreateLoader(id: Int, args: Bundle?): Loader<String> {
+ return TextLoader(requireContext())
+ }
+
+ override fun onLoadFinished(loader: Loader<String>, data: String) {
+ textView.text = data
+ }
+
+ override fun onLoaderReset(loader: Loader<String>) {}
+ }
+
+ companion object {
+ private const val TEXT_LOADER_ID = 14
+
+ val activity get() = RecreatedActivity.activity
+ }
+}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.java b/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.java
deleted file mode 100644
index 7cba3a7..0000000
--- a/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 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 androidx.fragment.app.test;
-
-import androidx.fragment.app.Fragment;
-import androidx.testutils.RecreatedActivity;
-
-public class NonConfigOnStopActivity extends RecreatedActivity {
- @Override
- protected void onStop() {
- super.onStop();
-
- getSupportFragmentManager()
- .beginTransaction()
- .add(new RetainedFragment(), "1")
- .commitNowAllowingStateLoss();
- }
-
- public static class RetainedFragment extends Fragment {
- public RetainedFragment() {
- setRetainInstance(true);
- }
- }
-}
diff --git a/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.kt b/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.kt
new file mode 100644
index 0000000..2c011c6
--- /dev/null
+++ b/fragment/src/androidTest/java/androidx/fragment/app/test/NonConfigOnStopActivity.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 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 androidx.fragment.app.test
+
+import androidx.fragment.app.Fragment
+import androidx.testutils.RecreatedActivity
+
+class NonConfigOnStopActivity : RecreatedActivity() {
+ override fun onStop() {
+ super.onStop()
+
+ supportFragmentManager
+ .beginTransaction()
+ .add(RetainedFragment(), "1")
+ .commitNowAllowingStateLoss()
+ }
+
+ class RetainedFragment : Fragment() {
+ init {
+ retainInstance = true
+ }
+ }
+}
diff --git a/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 6e52230..ff82951 100644
--- a/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -686,8 +686,10 @@
} else if (mFragmentManager != null && fragment.mFragmentManager != null) {
// Just save the reference to the Fragment
mTargetWho = fragment.mWho;
+ mTarget = null;
} else {
// Save the Fragment itself, waiting until we're attached
+ mTargetWho = null;
mTarget = fragment;
}
mTargetRequestCode = requestCode;
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java b/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
index a0801b3..717cade 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java
@@ -110,14 +110,10 @@
public FragmentActivity() {
super();
// Route onBackPressed() callbacks to the FragmentManager
- addOnBackPressedCallback(new OnBackPressedCallback() {
+ getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback() {
@Override
public boolean handleOnBackPressed() {
FragmentManager fragmentManager = mFragments.getSupportFragmentManager();
- if (fragmentManager.isStateSaved()) {
- // Cannot pop after state is saved
- return false;
- }
return fragmentManager.popBackStackImmediate();
}
});
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentTabHost.java b/fragment/src/main/java/androidx/fragment/app/FragmentTabHost.java
index 72b7a20..8f16d90 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentTabHost.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentTabHost.java
@@ -41,16 +41,10 @@
* the hierarchy you must call {@link #setup(Context, FragmentManager, int)}
* to complete the initialization of the tab host.
*
- * <p>Here is a simple example of using a FragmentTabHost in an Activity:
- *
- * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java
- * complete}
- *
- * <p>This can also be used inside of a fragment through fragment nesting:
- *
- * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java
- * complete}
+ * @deprecated Use <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
+@Deprecated
public class FragmentTabHost extends TabHost
implements TabHost.OnTabChangeListener {
private final ArrayList<TabInfo> mTabs = new ArrayList<>();
@@ -131,6 +125,12 @@
};
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
public FragmentTabHost(@NonNull Context context) {
// Note that we call through to the version that takes an AttributeSet,
// because the simple Context construct can result in a broken object!
@@ -138,6 +138,12 @@
initFragmentTabHost(context, null);
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
public FragmentTabHost(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initFragmentTabHost(context, attrs);
@@ -181,9 +187,9 @@
}
/**
- * @deprecated Don't call the original TabHost setup, you must instead
- * call {@link #setup(Context, FragmentManager)} or
- * {@link #setup(Context, FragmentManager, int)}.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Override @Deprecated
public void setup() {
@@ -193,7 +199,12 @@
/**
* Set up the FragmentTabHost to use the given FragmentManager
+ *
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
+ @Deprecated
public void setup(@NonNull Context context, @NonNull FragmentManager manager) {
ensureHierarchy(context); // Ensure views required by super.setup()
super.setup();
@@ -204,7 +215,12 @@
/**
* Set up the FragmentTabHost to use the given FragmentManager
+ *
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
+ @Deprecated
public void setup(@NonNull Context context, @NonNull FragmentManager manager,
int containerId) {
ensureHierarchy(context); // Ensure views required by super.setup()
@@ -232,11 +248,23 @@
}
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
public void setOnTabChangedListener(@Nullable OnTabChangeListener l) {
mOnTabChangeListener = l;
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
public void addTab(@NonNull TabHost.TabSpec tabSpec, @NonNull Class<?> clss,
@Nullable Bundle args) {
tabSpec.setContent(new DummyTabFactory(mContext));
@@ -260,6 +288,12 @@
addTab(tabSpec);
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -299,12 +333,24 @@
}
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mAttached = false;
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
@NonNull
protected Parcelable onSaveInstanceState() {
@@ -314,6 +360,12 @@
return ss;
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
protected void onRestoreInstanceState(@SuppressLint("UnknownNullness") Parcelable state) {
if (!(state instanceof SavedState)) {
@@ -325,6 +377,12 @@
setCurrentTabByTag(ss.curTab);
}
+ /**
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
+ */
+ @Deprecated
@Override
public void onTabChanged(@Nullable String tabId) {
if (mAttached) {
diff --git a/gradle.properties b/gradle.properties
index fd60892..c788833 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-org.gradle.jvmargs=-Xmx4g
+org.gradle.jvmargs=-Xmx8g
org.gradle.daemon=true
org.gradle.configureondemand=true
org.gradle.parallel=true
diff --git a/graphics/drawable/animated/build.gradle b/graphics/drawable/animated/build.gradle
index 4ce8b2a..fae5204 100644
--- a/graphics/drawable/animated/build.gradle
+++ b/graphics/drawable/animated/build.gradle
@@ -8,7 +8,7 @@
dependencies {
api(project(":vectordrawable"))
- implementation(project(":interpolator"))
+ implementation("androidx.interpolator:interpolator:1.0.0")
implementation(project(":collection"))
androidTestImplementation(TEST_EXT_JUNIT)
diff --git a/jetifier/jetifier/core/src/main/resources/default.config b/jetifier/jetifier/core/src/main/resources/default.config
index 52b33c3..4a18f8d 100644
--- a/jetifier/jetifier/core/src/main/resources/default.config
+++ b/jetifier/jetifier/core/src/main/resources/default.config
@@ -1534,14 +1534,38 @@
# "from": { "groupId": "android.arch.background.workmanager", "artifactId": "workmanager-firebase", "version": "{newArchVersion}" },
# "to": { "groupId": "androidx.work", "artifactId": "runtime-firebase", "version": "{newArchVersion}" }
#},
- #{
- # "from": { "groupId": "android.arch.navigation", "artifactId": "runtime", "version": "{newArchVersion}" },
- # "to": { "groupId": "androidx.navigation", "artifactId": "navigation-runtime", "version": "{newArchVersion}" }
- #},
- #{
- # "from": { "groupId": "android.arch.navigation", "artifactId": "fragment", "version": "{newArchVersion}" },
- # "to": { "groupId": "androidx.navigation", "artifactId": "navigation-fragment", "version": "{newArchVersion}" }
- #},
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "common", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-common", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "common-ktx", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-common-ktx", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "fragment", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-fragment", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "fragment-ktx", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-fragment-ktx", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "runtime", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-runtime", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "runtime-ktx", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-runtime-ktx", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "ui", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-ui", "version": "{newNavigationVersion}" }
+ },
+ {
+ "from": { "groupId": "android.arch.navigation", "artifactId": "ui-ktx", "version": "{oldNavigationVersion}" },
+ "to": { "groupId": "androidx.navigation", "artifactId": "navigation-ui-ktx", "version": "{newNavigationVersion}" }
+ },
{
"from": { "groupId": "android.arch.core", "artifactId": "common", "version": "1.1.1" },
"to": { "groupId": "androidx.arch.core", "artifactId": "core-common", "version": "{newArchCoreVersion}" }
@@ -1770,6 +1794,7 @@
"oldMedia2Version": "28.0.0-alpha03",
"oldExoplayerVersion": "28.0.0-alpha01",
"oldBiometricVersion": "28.0.0-alpha03",
+ "oldNavigationVersion": "1.0.0",
"newSlVersion": "1.0.0",
"newMaterialVersion": "1.0.0",
"newArchCoreVersion": "2.0.0",
@@ -1785,7 +1810,8 @@
"newMedia2Version": "1.0.0-alpha03",
"newExoplayerVersion": "1.0.0-alpha01",
"newBiometricVersion": "1.0.0-alpha03",
- "newDataBindingVersion": "undefined"
+ "newDataBindingVersion": "undefined",
+ "newNavigationVersion": "2.0.0"
}
},
# Manual fallback types map
diff --git a/jetifier/jetifier/core/src/main/resources/default.generated.config b/jetifier/jetifier/core/src/main/resources/default.generated.config
index 7c257f0..fd6d282 100644
--- a/jetifier/jetifier/core/src/main/resources/default.generated.config
+++ b/jetifier/jetifier/core/src/main/resources/default.generated.config
@@ -1928,6 +1928,102 @@
},
{
"from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "common",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-common",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "common-ktx",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-common-ktx",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "fragment",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-fragment",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "fragment-ktx",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-fragment-ktx",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "runtime",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-runtime",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "runtime-ktx",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-runtime-ktx",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "ui",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-ui",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
+ "groupId": "android.arch.navigation",
+ "artifactId": "ui-ktx",
+ "version": "{oldNavigationVersion}"
+ },
+ "to": {
+ "groupId": "androidx.navigation",
+ "artifactId": "navigation-ui-ktx",
+ "version": "{newNavigationVersion}"
+ }
+ },
+ {
+ "from": {
"groupId": "android.arch.core",
"artifactId": "common",
"version": "1.1.1"
@@ -2561,6 +2657,7 @@
"oldMedia2Version": "28.0.0-alpha03",
"oldExoplayerVersion": "28.0.0-alpha01",
"oldBiometricVersion": "28.0.0-alpha03",
+ "oldNavigationVersion": "1.0.0",
"newSlVersion": "1.0.0",
"newMaterialVersion": "1.0.0",
"newArchCoreVersion": "2.0.0",
@@ -2576,7 +2673,8 @@
"newMedia2Version": "1.0.0-alpha03",
"newExoplayerVersion": "1.0.0-alpha01",
"newBiometricVersion": "1.0.0-alpha03",
- "newDataBindingVersion": "undefined"
+ "newDataBindingVersion": "undefined",
+ "newNavigationVersion": "2.0.0"
}
},
"map": {
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index c1e50c9..2ba37b5 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -670,6 +670,14 @@
"to": "ignore"
},
{
+ "from": "androidx/legacy/app/ActionBarDrawerToggle(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/legacy/widget/Space(.*)",
+ "to": "ignore"
+ },
+ {
"from": "androidx/interpolator/view/animation/(.*)",
"to": "ignore"
},
@@ -722,6 +730,26 @@
"to": "ignore"
},
{
+ "from": "android/support/customtabs/ICustomTabsCallback(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "android/support/customtabs/ICustomTabsService(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "android/support/customtabs/IPostMessageService(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/browser/customtabs/(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/browser/R(.*)",
+ "to": "ignore"
+ },
+ {
"from": "androidx/legacy/view/ViewCompat(.*)",
"to": "ignore"
},
@@ -730,14 +758,6 @@
"to": "ignore"
},
{
- "from": "androidx/legacy/app/ActionBarDrawerToggle(.*)",
- "to": "ignore"
- },
- {
- "from": "androidx/legacy/widget/Space(.*)",
- "to": "ignore"
- },
- {
"from": "androidx/cardview/R(.*)",
"to": "ignore"
},
@@ -798,6 +818,18 @@
"to": "ignore"
},
{
+ "from": "androidx/leanback/preference/internal/OutlineOnlyWithChildrenFrameLayout(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/leanback/preference/(.*)",
+ "to": "ignore"
+ },
+ {
+ "from": "androidx/leanback/(.*)",
+ "to": "ignore"
+ },
+ {
"from": "androidx/print/(.*)",
"to": "ignore"
},
@@ -814,7 +846,7 @@
"to": "ignore"
},
{
- "from": "androidx/browser/(.*)",
+ "from": "androidx/browser/browseractions/(.*)",
"to": "ignore"
},
{
@@ -846,18 +878,6 @@
"to": "ignore"
},
{
- "from": "androidx/leanback/preference/internal/OutlineOnlyWithChildrenFrameLayout(.*)",
- "to": "ignore"
- },
- {
- "from": "androidx/leanback/preference/(.*)",
- "to": "ignore"
- },
- {
- "from": "androidx/leanback/(.*)",
- "to": "ignore"
- },
- {
"from": "androidx/media2/(.*)",
"to": "ignore"
},
diff --git a/legacy/v13/src/main/java/androidx/legacy/app/FragmentTabHost.java b/legacy/v13/src/main/java/androidx/legacy/app/FragmentTabHost.java
index 98de1bb..09f23eb 100644
--- a/legacy/v13/src/main/java/androidx/legacy/app/FragmentTabHost.java
+++ b/legacy/v13/src/main/java/androidx/legacy/app/FragmentTabHost.java
@@ -39,7 +39,8 @@
* used with the platform {@link android.app.Fragment} APIs. You will not
* normally use this, instead using action bar tabs.
*
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public class FragmentTabHost extends TabHost implements TabHost.OnTabChangeListener {
@@ -121,7 +122,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public FragmentTabHost(Context context) {
@@ -132,7 +135,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public FragmentTabHost(Context context, AttributeSet attrs) {
@@ -178,7 +183,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Override
@Deprecated
@@ -188,7 +195,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public void setup(Context context, FragmentManager manager) {
@@ -200,7 +209,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public void setup(Context context, FragmentManager manager, int containerId) {
@@ -230,7 +241,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -239,7 +252,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
@@ -265,7 +280,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -308,7 +325,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -318,7 +337,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -330,7 +351,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
@@ -345,7 +368,9 @@
}
/**
- * @deprecated Use {@link androidx.fragment.app.FragmentTabHost} instead.
+ * @deprecated Use
+ * <a href="https://developer.android.com/guide/navigation/navigation-swipe-view ">
+ * TabLayout and ViewPager</a> instead.
*/
@Deprecated
@Override
diff --git a/media/build.gradle b/media/build.gradle
index 26e120a..16eb997 100644
--- a/media/build.gradle
+++ b/media/build.gradle
@@ -7,7 +7,7 @@
}
dependencies {
- api(project(":core"))
+ api("androidx.core:core:1.1.0-alpha05")
implementation("androidx.collection:collection:1.0.0")
androidTestImplementation(TEST_EXT_JUNIT)
diff --git a/media2-widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java b/media2-widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
index e6acd7e..c381616 100644
--- a/media2-widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
+++ b/media2-widget/src/androidTest/java/androidx/media2/widget/MediaControlViewTest.java
@@ -462,7 +462,7 @@
}
private MediaItem createTestMediaItem2(Uri uri) {
- return new UriMediaItem.Builder(mVideoView.getContext(), uri).build();
+ return new UriMediaItem.Builder(uri).build();
}
private MediaController createController(MediaController.ControllerCallback callback) {
diff --git a/media2-widget/src/androidTest/java/androidx/media2/widget/VideoViewTest.java b/media2-widget/src/androidTest/java/androidx/media2/widget/VideoViewTest.java
index 5d4bf45..4f01d40 100644
--- a/media2-widget/src/androidTest/java/androidx/media2/widget/VideoViewTest.java
+++ b/media2-widget/src/androidTest/java/androidx/media2/widget/VideoViewTest.java
@@ -398,7 +398,6 @@
Uri testVideoUri = Uri.parse(
"android.resource://" + mContext.getPackageName() + "/"
+ R.raw.testvideo_with_2_subtitle_tracks);
- return new UriMediaItem.Builder(mVideoView.getContext(), testVideoUri)
- .build();
+ return new UriMediaItem.Builder(testVideoUri).build();
}
}
diff --git a/media2-widget/src/main/java/androidx/media2/widget/MediaControlView.java b/media2-widget/src/main/java/androidx/media2/widget/MediaControlView.java
index dcb8bf8..37d53e9 100644
--- a/media2-widget/src/main/java/androidx/media2/widget/MediaControlView.java
+++ b/media2-widget/src/main/java/androidx/media2/widget/MediaControlView.java
@@ -1243,16 +1243,8 @@
if (position != mSelectedSubtitleTrackIndex) {
if (position > 0) {
mController.showSubtitle(position - 1);
- mSubtitleButton.setImageDrawable(
- mResources.getDrawable(R.drawable.ic_subtitle_on));
- mSubtitleButton.setContentDescription(
- mResources.getString(R.string.mcv2_cc_is_on));
} else {
mController.hideSubtitle();
- mSubtitleButton.setImageDrawable(
- mResources.getDrawable(R.drawable.ic_subtitle_off));
- mSubtitleButton.setContentDescription(
- mResources.getString(R.string.mcv2_cc_is_off));
}
}
dismissSettingsWindow();
@@ -2211,12 +2203,20 @@
if (mSettingsMode == SETTINGS_MODE_SUBTITLE_TRACK) {
mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex);
}
+ mSubtitleButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_subtitle_on));
+ mSubtitleButton.setContentDescription(
+ mResources.getString(R.string.mcv2_cc_is_on));
break;
case EVENT_UPDATE_SUBTITLE_DESELECTED:
mSelectedSubtitleTrackIndex = 0;
if (mSettingsMode == SETTINGS_MODE_SUBTITLE_TRACK) {
mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex);
}
+ mSubtitleButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_subtitle_off));
+ mSubtitleButton.setContentDescription(
+ mResources.getString(R.string.mcv2_cc_is_off));
break;
default:
return new SessionResult(
diff --git a/media2-widget/src/main/java/androidx/media2/widget/VideoView.java b/media2-widget/src/main/java/androidx/media2/widget/VideoView.java
index cf1ba6f..0ab34d2 100644
--- a/media2-widget/src/main/java/androidx/media2/widget/VideoView.java
+++ b/media2-widget/src/main/java/androidx/media2/widget/VideoView.java
@@ -197,7 +197,7 @@
int mSelectedAudioTrackIndex;
int mSelectedSubtitleTrackIndex;
- private SubtitleAnchorView mSubtitleAnchorView;
+ SubtitleAnchorView mSubtitleAnchorView;
private MediaRouter mMediaRouter;
@SuppressWarnings("WeakerAccess") /* synthetic access */
@@ -296,9 +296,6 @@
if (DEBUG) {
Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
}
- if (mCurrentState != STATE_PLAYING && mMediaSession != null) {
- mMediaSession.getPlayer().seekTo(mMediaSession.getPlayer().getCurrentPosition());
- }
if (view != mCurrentView) {
((View) mCurrentView).setVisibility(View.GONE);
mCurrentView = view;
@@ -467,7 +464,14 @@
/**
* Selects which view will be used to render video between SurfaceView and TextureView.
- *
+ * <p>
+ * Note: There are two known issues on API level 28+ devices.
+ * <ul>
+ * <li> When changing view type to SurfaceView from TextureView in "paused" playback state,
+ * a blank screen can be shown.
+ * <li> When changing view type to TextureView from SurfaceView repeatedly in "paused" playback
+ * state, the lastly rendered frame on TextureView can be shown.
+ * </ul>
* @param viewType the view type to render video
* <ul>
* <li>{@link #VIEW_TYPE_SURFACEVIEW}
@@ -685,7 +689,35 @@
mMediaPlayer.setMediaItem(mMediaItem);
final Context context = getContext();
- mSubtitleController = new SubtitleController(context);
+ SubtitleController.Listener listener = new SubtitleController.Listener() {
+ @Override
+ public void onSubtitleTrackSelected(SubtitleTrack track) {
+ if (track == null) {
+ mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
+ mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+ mSubtitleAnchorView.setVisibility(View.GONE);
+
+ mMediaSession.broadcastCustomCommand(new SessionCommand(
+ MediaControlView.EVENT_UPDATE_SUBTITLE_DESELECTED, null), null);
+ return;
+ }
+ int indexInSubtitleTrackList = mSubtitleTracks.indexOfValue(track);
+ if (indexInSubtitleTrackList >= 0) {
+ int indexInEntireTrackList =
+ mSubtitleTracks.keyAt(indexInSubtitleTrackList);
+ mMediaPlayer.selectTrack(indexInEntireTrackList);
+ mSelectedSubtitleTrackIndex = indexInEntireTrackList;
+ mSubtitleAnchorView.setVisibility(View.VISIBLE);
+
+ Bundle data = new Bundle();
+ data.putInt(MediaControlView.KEY_SELECTED_SUBTITLE_INDEX,
+ indexInSubtitleTrackList);
+ mMediaSession.broadcastCustomCommand(new SessionCommand(
+ MediaControlView.EVENT_UPDATE_SUBTITLE_SELECTED, null), data);
+ }
+ }
+ };
+ mSubtitleController = new SubtitleController(context, null, listener);
mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
mSubtitleController.registerRenderer(new Cea708CaptionRenderer(context));
mSubtitleController.setAnchor(mSubtitleAnchorView);
@@ -728,17 +760,7 @@
}
SubtitleTrack track = mSubtitleTracks.get(trackIndex);
if (track != null) {
- mMediaPlayer.selectTrack(trackIndex);
mSubtitleController.selectTrack(track);
- mSelectedSubtitleTrackIndex = trackIndex;
- mSubtitleAnchorView.setVisibility(View.VISIBLE);
-
- Bundle data = new Bundle();
- data.putInt(MediaControlView.KEY_SELECTED_SUBTITLE_INDEX,
- mSubtitleTracks.indexOfKey(trackIndex));
- mMediaSession.broadcastCustomCommand(
- new SessionCommand(MediaControlView.EVENT_UPDATE_SUBTITLE_SELECTED, null),
- data);
}
}
@@ -746,13 +768,7 @@
if (!isMediaPrepared() || mSelectedSubtitleTrackIndex == INVALID_TRACK_INDEX) {
return;
}
- mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
- mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
- mSubtitleAnchorView.setVisibility(View.GONE);
-
- mMediaSession.broadcastCustomCommand(
- new SessionCommand(MediaControlView.EVENT_UPDATE_SUBTITLE_DESELECTED, null),
- null);
+ mSubtitleController.selectTrack(null);
}
// TODO: move this method inside callback to make sure it runs inside the callback thread.
@@ -762,6 +778,7 @@
mAudioTrackIndices = new ArrayList<>();
mSubtitleTracks = new SparseArray<>();
ArrayList<String> subtitleTracksLanguageList = new ArrayList<>();
+ int selectedSubtitleTrackIndex = mSelectedSubtitleTrackIndex;
mSubtitleController.reset();
for (int i = 0; i < trackInfos.size(); ++i) {
int trackType = trackInfos.get(i).getTrackType();
@@ -773,9 +790,7 @@
SubtitleTrack track = mSubtitleController.addTrack(trackInfos.get(i).getFormat());
if (track != null) {
mSubtitleTracks.put(i, track);
- String language =
- (trackInfos.get(i).getLanguage().equals(SUBTITLE_TRACK_LANG_UNDEFINED))
- ? "" : trackInfos.get(i).getLanguage();
+ String language = trackInfos.get(i).getLanguage().getISO3Language();
subtitleTracksLanguageList.add(language);
}
}
@@ -784,6 +799,10 @@
if (mAudioTrackIndices.size() > 0) {
mSelectedAudioTrackIndex = 0;
}
+ // Re-select originally selected subtitle track since SubtitleController has been reset.
+ if (selectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
+ selectSubtitleTrack(selectedSubtitleTrackIndex);
+ }
Bundle data = new Bundle();
data.putInt(MediaControlView.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
@@ -1059,13 +1078,14 @@
}
switch (customCommand.getCustomCommand()) {
case MediaControlView.COMMAND_SHOW_SUBTITLE:
- int subtitleIndex = args != null ? args.getInt(
+ int indexInSubtitleTrackList = args != null ? args.getInt(
MediaControlView.KEY_SELECTED_SUBTITLE_INDEX,
INVALID_TRACK_INDEX) : INVALID_TRACK_INDEX;
- if (subtitleIndex != INVALID_TRACK_INDEX) {
- int subtitleTrackIndex = mSubtitleTracks.keyAt(subtitleIndex);
- if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
- selectSubtitleTrack(subtitleTrackIndex);
+ if (indexInSubtitleTrackList != INVALID_TRACK_INDEX) {
+ int indexInEntireTrackList =
+ mSubtitleTracks.keyAt(indexInSubtitleTrackList);
+ if (indexInEntireTrackList != mSelectedSubtitleTrackIndex) {
+ selectSubtitleTrack(indexInEntireTrackList);
}
}
break;
diff --git a/media2/api/1.0.0-alpha05.txt b/media2/api/1.0.0-alpha05.txt
index ba118d1..d753947 100644
--- a/media2/api/1.0.0-alpha05.txt
+++ b/media2/api/1.0.0-alpha05.txt
@@ -316,7 +316,7 @@
method public float getMaxPlayerVolume();
method public int getNextMediaItemIndex();
method public androidx.media2.PlaybackParams getPlaybackParams();
- method public float getPlaybackSpeed();
+ method @FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) public float getPlaybackSpeed();
method public int getPlayerState();
method public float getPlayerVolume();
method public java.util.List<androidx.media2.MediaItem>? getPlaylist();
@@ -331,6 +331,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> pause();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> play();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> prepare();
+ method public void registerPlayerCallback(java.util.concurrent.Executor, androidx.media2.MediaPlayer.PlayerCallback);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> removePlaylistItem(@IntRange(from=0) int);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> replacePlaylistItem(int, androidx.media2.MediaItem);
method public void reset();
@@ -342,7 +343,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setAuxEffectSendLevel(@FloatRange(from=0, to=1) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setMediaItem(androidx.media2.MediaItem);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackParams(androidx.media2.PlaybackParams);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackSpeed(@FloatRange(from=0, to=1) float);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackSpeed(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlayerVolume(@FloatRange(from=0, to=1) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaylist(java.util.List<androidx.media2.MediaItem>, androidx.media2.MediaMetadata?);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setRepeatMode(int);
@@ -351,6 +352,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToNextPlaylistItem();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToPlaylistItem(@IntRange(from=0) int);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToPreviousPlaylistItem();
+ method public void unregisterPlayerCallback(androidx.media2.MediaPlayer.PlayerCallback);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> updatePlaylistMetadata(androidx.media2.MediaMetadata?);
field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
@@ -360,6 +362,7 @@
field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
+ field public static final int NO_TRACK_SELECTED = -2147483648; // 0x80000000
field public static final int PLAYER_ERROR_IO = -1004; // 0xfffffc14
field public static final int PLAYER_ERROR_MALFORMED = -1007; // 0xfffffc11
field public static final int PLAYER_ERROR_TIMED_OUT = -110; // 0xffffff92
@@ -383,7 +386,7 @@
public static final class MediaPlayer.TrackInfo {
method public android.media.MediaFormat? getFormat();
- method public String getLanguage();
+ method public java.util.Locale getLanguage();
method public int getTrackType();
field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
@@ -501,8 +504,8 @@
ctor public PlaybackParams.Builder(androidx.media2.PlaybackParams);
method public androidx.media2.PlaybackParams build();
method public androidx.media2.PlaybackParams.Builder setAudioFallbackMode(int);
- method public androidx.media2.PlaybackParams.Builder setPitch(float);
- method public androidx.media2.PlaybackParams.Builder setSpeed(float);
+ method public androidx.media2.PlaybackParams.Builder setPitch(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE) float);
+ method public androidx.media2.PlaybackParams.Builder setSpeed(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) float);
}
public interface Rating extends androidx.versionedparcelable.VersionedParcelable {
@@ -724,14 +727,13 @@
public class UriMediaItem extends androidx.media2.MediaItem {
method public android.net.Uri getUri();
- method public android.content.Context getUriContext();
method public java.util.List<java.net.HttpCookie>? getUriCookies();
method public java.util.Map<java.lang.String,java.lang.String>? getUriHeaders();
}
public static final class UriMediaItem.Builder extends androidx.media2.MediaItem.Builder {
- ctor public UriMediaItem.Builder(android.content.Context, android.net.Uri);
- ctor public UriMediaItem.Builder(android.content.Context, android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
+ ctor public UriMediaItem.Builder(android.net.Uri);
+ ctor public UriMediaItem.Builder(android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
method public androidx.media2.UriMediaItem build();
method public androidx.media2.UriMediaItem.Builder setEndPosition(long);
method public androidx.media2.UriMediaItem.Builder setMetadata(androidx.media2.MediaMetadata?);
diff --git a/media2/api/current.txt b/media2/api/current.txt
index ba118d1..d753947 100644
--- a/media2/api/current.txt
+++ b/media2/api/current.txt
@@ -316,7 +316,7 @@
method public float getMaxPlayerVolume();
method public int getNextMediaItemIndex();
method public androidx.media2.PlaybackParams getPlaybackParams();
- method public float getPlaybackSpeed();
+ method @FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) public float getPlaybackSpeed();
method public int getPlayerState();
method public float getPlayerVolume();
method public java.util.List<androidx.media2.MediaItem>? getPlaylist();
@@ -331,6 +331,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> pause();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> play();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> prepare();
+ method public void registerPlayerCallback(java.util.concurrent.Executor, androidx.media2.MediaPlayer.PlayerCallback);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> removePlaylistItem(@IntRange(from=0) int);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> replacePlaylistItem(int, androidx.media2.MediaItem);
method public void reset();
@@ -342,7 +343,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setAuxEffectSendLevel(@FloatRange(from=0, to=1) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setMediaItem(androidx.media2.MediaItem);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackParams(androidx.media2.PlaybackParams);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackSpeed(@FloatRange(from=0, to=1) float);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaybackSpeed(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlayerVolume(@FloatRange(from=0, to=1) float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setPlaylist(java.util.List<androidx.media2.MediaItem>, androidx.media2.MediaMetadata?);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> setRepeatMode(int);
@@ -351,6 +352,7 @@
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToNextPlaylistItem();
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToPlaylistItem(@IntRange(from=0) int);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> skipToPreviousPlaylistItem();
+ method public void unregisterPlayerCallback(androidx.media2.MediaPlayer.PlayerCallback);
method public com.google.common.util.concurrent.ListenableFuture<androidx.media2.SessionPlayer.PlayerResult> updatePlaylistMetadata(androidx.media2.MediaMetadata?);
field public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804; // 0x324
field public static final int MEDIA_INFO_BAD_INTERLEAVING = 800; // 0x320
@@ -360,6 +362,7 @@
field public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805; // 0x325
field public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3; // 0x3
field public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700; // 0x2bc
+ field public static final int NO_TRACK_SELECTED = -2147483648; // 0x80000000
field public static final int PLAYER_ERROR_IO = -1004; // 0xfffffc14
field public static final int PLAYER_ERROR_MALFORMED = -1007; // 0xfffffc11
field public static final int PLAYER_ERROR_TIMED_OUT = -110; // 0xffffff92
@@ -383,7 +386,7 @@
public static final class MediaPlayer.TrackInfo {
method public android.media.MediaFormat? getFormat();
- method public String getLanguage();
+ method public java.util.Locale getLanguage();
method public int getTrackType();
field public static final int MEDIA_TRACK_TYPE_AUDIO = 2; // 0x2
field public static final int MEDIA_TRACK_TYPE_METADATA = 5; // 0x5
@@ -501,8 +504,8 @@
ctor public PlaybackParams.Builder(androidx.media2.PlaybackParams);
method public androidx.media2.PlaybackParams build();
method public androidx.media2.PlaybackParams.Builder setAudioFallbackMode(int);
- method public androidx.media2.PlaybackParams.Builder setPitch(float);
- method public androidx.media2.PlaybackParams.Builder setSpeed(float);
+ method public androidx.media2.PlaybackParams.Builder setPitch(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE) float);
+ method public androidx.media2.PlaybackParams.Builder setSpeed(@FloatRange(from=0.0f, to=java.lang.Float.MAX_VALUE, fromInclusive=false) float);
}
public interface Rating extends androidx.versionedparcelable.VersionedParcelable {
@@ -724,14 +727,13 @@
public class UriMediaItem extends androidx.media2.MediaItem {
method public android.net.Uri getUri();
- method public android.content.Context getUriContext();
method public java.util.List<java.net.HttpCookie>? getUriCookies();
method public java.util.Map<java.lang.String,java.lang.String>? getUriHeaders();
}
public static final class UriMediaItem.Builder extends androidx.media2.MediaItem.Builder {
- ctor public UriMediaItem.Builder(android.content.Context, android.net.Uri);
- ctor public UriMediaItem.Builder(android.content.Context, android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
+ ctor public UriMediaItem.Builder(android.net.Uri);
+ ctor public UriMediaItem.Builder(android.net.Uri, java.util.Map<java.lang.String,java.lang.String>?, java.util.List<java.net.HttpCookie>?);
method public androidx.media2.UriMediaItem build();
method public androidx.media2.UriMediaItem.Builder setEndPosition(long);
method public androidx.media2.UriMediaItem.Builder setMetadata(androidx.media2.MediaMetadata?);
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayer2DrmTestBase.java b/media2/src/androidTest/java/androidx/media2/MediaPlayer2DrmTestBase.java
index cd59aca..15eb2ba 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayer2DrmTestBase.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayer2DrmTestBase.java
@@ -302,8 +302,7 @@
mPlayer.setEventCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
- mPlayer.setMediaItem(
- new UriMediaItem.Builder(mContext, file).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
mSetDataSourceCallCompleted.waitForSignal();
if (mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR) {
throw new PrepareFailedException();
@@ -557,8 +556,7 @@
mPlayer.setEventCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
- mPlayer.setMediaItem(
- new UriMediaItem.Builder(mContext, file).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
Log.v(TAG, "playLoadedVideo: prepare()");
mPlayer.prepare();
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java b/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
index 1ac7d97..d7827d1 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayer2Test.java
@@ -219,7 +219,7 @@
// test stop and restart
mp.reset();
mp.setEventCallback(mExecutor, ecb);
- mp.setMediaItem(new UriMediaItem.Builder(mContext, uri).build());
+ mp.setMediaItem(new UriMediaItem.Builder(uri).build());
onPrepareCalled.reset();
mp.prepare();
onPrepareCalled.waitForSignal();
@@ -2335,7 +2335,7 @@
Uri uri = Uri.parse(outputFileLocation);
MediaPlayer2 mp = MediaPlayer2.create(mActivity);
try {
- mp.setMediaItem(new UriMediaItem.Builder(mContext, uri).build());
+ mp.setMediaItem(new UriMediaItem.Builder(uri).build());
mp.prepare();
Thread.sleep(SLEEP_TIME);
playAndStop(mp);
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayer2TestBase.java b/media2/src/androidTest/java/androidx/media2/MediaPlayer2TestBase.java
index c473620..13dd115 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayer2TestBase.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayer2TestBase.java
@@ -121,7 +121,7 @@
new AudioAttributesCompat.Builder().build();
mp.setAudioAttributes(aa);
mp.setAudioSessionId(audioSessionId);
- mp.setMediaItem(new UriMediaItem.Builder(context, uri).build());
+ mp.setMediaItem(new UriMediaItem.Builder(uri).build());
if (holder != null) {
mp.setSurface(holder.getSurface());
}
@@ -417,7 +417,7 @@
final Uri uri = Uri.parse(path);
for (int i = 0; i < STREAM_RETRIES; i++) {
try {
- mPlayer.setMediaItem(new UriMediaItem.Builder(mContext, uri).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(uri).build());
playLoadedVideo(width, height, playTime);
playedSuccessfully = true;
break;
@@ -449,8 +449,7 @@
boolean playedSuccessfully = false;
for (int i = 0; i < STREAM_RETRIES; i++) {
try {
- mPlayer.setMediaItem(new UriMediaItem.Builder(
- mContext, uri, headers, cookies).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(uri, headers, cookies).build());
playLoadedVideo(width, height, playTime);
playedSuccessfully = true;
break;
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java b/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
index 0195c56..ca8caec 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayerDrmTest.java
@@ -402,7 +402,7 @@
mPlayer.registerPlayerCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
ListenableFuture<PlayerResult> future =
- mPlayer.setMediaItem(new UriMediaItem.Builder(mContext, file).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
assertEquals(PlayerResult.RESULT_SUCCESS, future.get().getResultCode());
SurfaceHolder surfaceHolder = mActivity.getSurfaceHolder();
@@ -645,8 +645,7 @@
mPlayer.registerPlayerCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
- mPlayer.setMediaItem(
- new UriMediaItem.Builder(mContext, file).build());
+ mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
Log.v(TAG, "playLoadedVideo: prepare()");
ListenableFuture<PlayerResult> future = mPlayer.prepare();
diff --git a/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java b/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
index 50cbb19..9a19cb9 100644
--- a/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
+++ b/media2/src/androidTest/java/androidx/media2/MediaPlayerTestBase.java
@@ -117,7 +117,7 @@
.build();
return mPlayer.setMediaItem(new UriMediaItem.Builder(
- mContext, testVideoUri).build()).get().getResultCode()
+ testVideoUri).build()).get().getResultCode()
== androidx.media2.SessionPlayer.PlayerResult.RESULT_SUCCESS;
}
diff --git a/media2/src/main/java/androidx/media2/MediaPlayer.java b/media2/src/main/java/androidx/media2/MediaPlayer.java
index f1d2383..50add35 100644
--- a/media2/src/main/java/androidx/media2/MediaPlayer.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer.java
@@ -62,6 +62,7 @@
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
@@ -127,6 +128,10 @@
* <td>This is to handle error</td></tr>
* </table>
* <p>
+ * If an {@link AudioAttributesCompat} is not specified by {@link #setAudioAttributes},
+ * {@link #getAudioAttributes} will return {@code null} and the default audio focus behavior will
+ * follow the {@code null} case on the table above.
+ * <p>
* For more information about the audio focus, take a look at
* <a href="{@docRoot}guide/topics/media-apps/audio-focus.html">Managing audio focus</a>
* <p>
@@ -426,6 +431,13 @@
@RestrictTo(LIBRARY_GROUP_PREFIX)
public @interface SeekMode {}
+ /**
+ * The return value of {@link #getSelectedTrack} when there is no selected track for the given
+ * type.
+ * @see #getSelectedTrack(int)
+ */
+ public static final int NO_TRACK_SELECTED = Integer.MIN_VALUE;
+
private static final int CALL_COMPLETE_PLAYLIST_BASE = -1000;
private static final int END_OF_PLAYLIST = -1;
private static final int NO_MEDIA_ITEM = -2;
@@ -805,7 +817,8 @@
@Override
@NonNull
public ListenableFuture<PlayerResult> setPlaybackSpeed(
- @FloatRange(from = 0, to = 1) final float playbackSpeed) {
+ @FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false)
+ final float playbackSpeed) {
PendingFuture<PlayerResult> pendingFuture = new PendingFuture<PlayerResult>(mExecutor) {
@Override
List<ResolvableFuture<PlayerResult>> onExecute() {
@@ -849,7 +862,8 @@
}
@Override
- public @PlayerState int getPlayerState() {
+ @PlayerState
+ public int getPlayerState() {
synchronized (mStateLock) {
return mState;
}
@@ -895,7 +909,8 @@
}
@Override
- public @BuffState int getBufferingState() {
+ @BuffState
+ public int getBufferingState() {
Integer buffState;
synchronized (mStateLock) {
buffState = mMediaItemToBuffState.get(mPlayer.getCurrentMediaItem());
@@ -904,6 +919,7 @@
}
@Override
+ @FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false)
public float getPlaybackSpeed() {
try {
return mPlayer.getPlaybackParams().getSpeed();
@@ -1611,7 +1627,8 @@
* receive a notification {@link PlayerCallback#onVideoSizeChanged} when the size
* is available.
*/
- public @NonNull VideoSize getVideoSize() {
+ @NonNull
+ public VideoSize getVideoSize() {
return new VideoSize(mPlayer.getVideoWidth(), mPlayer.getVideoHeight());
}
@@ -1630,13 +1647,7 @@
}
/**
- * Sets playback rate using {@link PlaybackParams}.
- * <p>
- * The player sets its internal PlaybackParams to the given input. This does not change the
- * player state. For example, if this is called with the speed of 2.0f in
- * {@link #PLAYER_STATE_PAUSED}, the player will just update internal property and stay paused.
- * Once the client calls {@link #play()} afterwards, the player will start playback with the
- * given speed. Calling this with zero speed is not allowed.
+ * Sets playback params using {@link PlaybackParams}.
*
* @param params the playback params.
* @return a {@link ListenableFuture} which represents the pending completion of the command.
@@ -1887,8 +1898,9 @@
* @see #selectTrack(int)
* @see #deselectTrack(int)
*/
- public int getSelectedTrack(int trackType) {
- return mPlayer.getSelectedTrack(trackType);
+ public int getSelectedTrack(@TrackInfo.MediaTrackType int trackType) {
+ final int ret = mPlayer.getSelectedTrack(trackType);
+ return (ret < 0) ? NO_TRACK_SELECTED : ret;
}
/**
@@ -1974,6 +1986,29 @@
}
/**
+ * Register {@link PlayerCallback} to listen changes.
+ *
+ * @param executor a callback Executor
+ * @param callback a PlayerCallback
+ * @throws IllegalArgumentException if executor or callback is {@code null}.
+ */
+ public void registerPlayerCallback(
+ @NonNull /*@CallbackExecutor*/ Executor executor,
+ @NonNull PlayerCallback callback) {
+ super.registerPlayerCallback(executor, callback);
+ }
+
+ /**
+ * Unregister the previously registered {@link PlayerCallback}.
+ *
+ * @param callback the callback to be removed
+ * @throws IllegalArgumentException if the callback is {@code null}.
+ */
+ public void unregisterPlayerCallback(@NonNull PlayerCallback callback) {
+ super.unregisterPlayerCallback(callback);
+ }
+
+ /**
* Retrieves the DRM Info associated with the current media item.
*
* @throws IllegalStateException if called before being prepared
@@ -2793,6 +2828,20 @@
public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
public static final int MEDIA_TRACK_TYPE_METADATA = 5;
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, /*prefix = "PLAYER_ERROR",*/ value = {
+ MEDIA_TRACK_TYPE_UNKNOWN,
+ MEDIA_TRACK_TYPE_VIDEO,
+ MEDIA_TRACK_TYPE_AUDIO,
+ MEDIA_TRACK_TYPE_SUBTITLE,
+ MEDIA_TRACK_TYPE_METADATA,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP_PREFIX)
+ public @interface MediaTrackType {}
+
private final int mTrackType;
private final MediaFormat mFormat;
@@ -2800,20 +2849,21 @@
* Gets the track type.
* @return TrackType which indicates if the track is video, audio, timed text.
*/
- public int getTrackType() {
+ public @MediaTrackType int getTrackType() {
return mTrackType;
}
/**
* Gets the language code of the track.
- * @return a language code in either way of ISO-639-1 or ISO-639-2.
- * When the language is unknown or could not be determined,
- * ISO-639-2 language code, "und", is returned.
+ * @return {@link Locale} which includes the language information.
*/
@NonNull
- public String getLanguage() {
- String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
- return language == null ? "und" : language;
+ public Locale getLanguage() {
+ String language = mFormat != null ? mFormat.getString(MediaFormat.KEY_LANGUAGE) : null;
+ if (language == null) {
+ language = "und";
+ }
+ return new Locale(language);
}
/**
diff --git a/media2/src/main/java/androidx/media2/MediaPlayer2.java b/media2/src/main/java/androidx/media2/MediaPlayer2.java
index 243d7d1..690d3c1 100644
--- a/media2/src/main/java/androidx/media2/MediaPlayer2.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer2.java
@@ -243,7 +243,7 @@
if (Build.VERSION.SDK_INT <= 27 || DEBUG_USE_EXOPLAYER) {
return new ExoPlayerMediaPlayer2Impl(context);
} else {
- return new MediaPlayer2Impl();
+ return new MediaPlayer2Impl(context);
}
}
diff --git a/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java b/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
index 8ead1fe..7816fe1 100644
--- a/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
+++ b/media2/src/main/java/androidx/media2/MediaPlayer2Impl.java
@@ -17,6 +17,7 @@
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+import android.content.Context;
import android.media.AudioAttributes;
import android.media.DeniedByServerException;
import android.media.MediaDataSource;
@@ -138,6 +139,7 @@
MediaPlayerSourceQueue mPlayer;
private HandlerThread mHandlerThread;
+ private final Context mContext;
private final Handler mEndPositionHandler;
private final Handler mTaskHandler;
@SuppressWarnings("WeakerAccess") /* synthetic access */
@@ -174,7 +176,8 @@
* to free the resources. If not released, too many MediaPlayer2Impl instances may
* result in an exception.</p>
*/
- public MediaPlayer2Impl() {
+ public MediaPlayer2Impl(Context context) {
+ mContext = context;
mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
mHandlerThread.start();
Looper looper = mHandlerThread.getLooper();
@@ -451,7 +454,7 @@
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
- static void handleDataSource(MediaPlayerSource src)
+ void handleDataSource(MediaPlayerSource src)
throws IOException {
final MediaItem item = src.getDSD();
Preconditions.checkArgument(item != null, "the MediaItem cannot be null");
@@ -488,7 +491,7 @@
} else if (item instanceof UriMediaItem) {
UriMediaItem uitem = (UriMediaItem) item;
player.setDataSource(
- uitem.getUriContext(),
+ mContext,
uitem.getUri(),
uitem.getUriHeaders(),
uitem.getUriCookies());
diff --git a/media2/src/main/java/androidx/media2/PlaybackParams.java b/media2/src/main/java/androidx/media2/PlaybackParams.java
index 2b93cd6..b3d4380 100644
--- a/media2/src/main/java/androidx/media2/PlaybackParams.java
+++ b/media2/src/main/java/androidx/media2/PlaybackParams.java
@@ -21,6 +21,7 @@
import android.media.AudioTrack;
import android.os.Build;
+import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -36,8 +37,19 @@
* Used by {@link MediaPlayer} {@link MediaPlayer#getPlaybackParams()} and
* {@link MediaPlayer#setPlaybackParams(PlaybackParams)}
* to control playback behavior.
- * <p> <strong>audio fallback mode:</strong>
- * select out-of-range parameter handling.
+ * <p>
+ * PlaybackParams returned by {@link MediaPlayer#getPlaybackParams()} will always have values.
+ * In case of {@link MediaPlayer#setPlaybackParams}, the player will not update the param if the
+ * value is not set. For example, if pitch is set while speed is not set, only pitch will be
+ * updated.
+ * <p>
+ * Note that the speed value does not change the player state. For example, if
+ * {@link MediaPlayer#getPlaybackParams()} is called with the speed of 2.0f in
+ * {@link MediaPlayer#PLAYER_STATE_PAUSED}, the player will just update internal property and stay
+ * paused. Once {@link MediaPlayer#play()} is called afterwards, the player will start
+ * playback with the given speed. Calling this with zero speed is not allowed.
+ * <p>
+ * <strong>audio fallback mode:</strong> select out-of-range parameter handling.
* <ul>
* <li> {@link PlaybackParams#AUDIO_FALLBACK_MODE_DEFAULT}:
* System will determine best handling. </li>
@@ -219,7 +231,8 @@
* @return this <code>Builder</code> instance.
* @throws IllegalArgumentException if the pitch is negative.
*/
- public @NonNull Builder setPitch(float pitch) {
+ public @NonNull Builder setPitch(
+ @FloatRange(from = 0.0f, to = Float.MAX_VALUE) float pitch) {
if (pitch < 0.f) {
throw new IllegalArgumentException("pitch must not be negative");
}
@@ -236,7 +249,14 @@
*
* @return this <code>Builder</code> instance.
*/
- public @NonNull Builder setSpeed(float speed) {
+ public @NonNull Builder setSpeed(
+ @FloatRange(from = 0.0f, to = Float.MAX_VALUE, fromInclusive = false) float speed) {
+ if (speed == 0.f) {
+ throw new IllegalArgumentException("0 speed is not allowed.");
+ }
+ if (speed < 0.f) {
+ throw new IllegalArgumentException("negative speed is not supported.");
+ }
if (Build.VERSION.SDK_INT >= 23) {
mPlaybackParams.setSpeed(speed);
} else {
diff --git a/media2/src/main/java/androidx/media2/UriMediaItem.java b/media2/src/main/java/androidx/media2/UriMediaItem.java
index a867f40..f7d55b6 100644
--- a/media2/src/main/java/androidx/media2/UriMediaItem.java
+++ b/media2/src/main/java/androidx/media2/UriMediaItem.java
@@ -16,7 +16,6 @@
package androidx.media2;
-import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
@@ -55,9 +54,6 @@
@NonParcelField
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<HttpCookie> mUriCookies;
- @NonParcelField
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- Context mUriContext;
/**
* Used for VersionedParcelable
@@ -71,7 +67,6 @@
mUri = builder.mUri;
mUriHeader = builder.mUriHeader;
mUriCookies = builder.mUriCookies;
- mUriContext = builder.mUriContext;
}
/**
@@ -105,14 +100,6 @@
}
/**
- * Return the Context used for resolving the Uri of this media item.
- * @return the Context used for resolving the Uri of this media item
- */
- public @NonNull Context getUriContext() {
- return mUriContext;
- }
-
- /**
* This Builder class simplifies the creation of a {@link UriMediaItem} object.
*/
public static final class Builder extends MediaItem.Builder {
@@ -123,17 +110,14 @@
Map<String, String> mUriHeader;
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<HttpCookie> mUriCookies;
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- Context mUriContext;
/**
* Creates a new Builder object with a content Uri.
*
- * @param context the Context to use when resolving the Uri
* @param uri the Content URI of the data you want to play
*/
- public Builder(@NonNull Context context, @NonNull Uri uri) {
- this(context, uri, null, null);
+ public Builder(@NonNull Uri uri) {
+ this(uri, null, null);
}
/**
@@ -147,7 +131,6 @@
* "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
* disallow or allow cross domain redirection.
*
- * @param context the Context to use when resolving the Uri
* @param uri the Content URI of the data you want to play
* @param headers the headers to be sent together with the request for the data
* The headers must not include cookies. Instead, use the cookies param.
@@ -155,12 +138,10 @@
* @throws IllegalArgumentException if the cookie handler is not of CookieManager type
* when cookies are provided.
*/
- public Builder(@NonNull Context context, @NonNull Uri uri,
- @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
- Preconditions.checkNotNull(context, "context cannot be null");
+ public Builder(@NonNull Uri uri, @Nullable Map<String, String> headers,
+ @Nullable List<HttpCookie> cookies) {
Preconditions.checkNotNull(uri, "uri cannot be null");
mUri = uri;
- mUriContext = context;
if (cookies != null) {
CookieHandler cookieHandler = CookieHandler.getDefault();
if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
@@ -177,7 +158,6 @@
if (cookies != null) {
mUriCookies = new ArrayList<HttpCookie>(cookies);
}
- mUriContext = context;
}
// Override just to change return type.
diff --git a/media2/src/main/java/androidx/media2/subtitle/SubtitleController.java b/media2/src/main/java/androidx/media2/subtitle/SubtitleController.java
index 7b246fa..80028ca 100644
--- a/media2/src/main/java/androidx/media2/subtitle/SubtitleController.java
+++ b/media2/src/main/java/androidx/media2/subtitle/SubtitleController.java
@@ -523,7 +523,10 @@
}
}
- interface Listener {
+ /**
+ * Listener for when subtitle track has been selected.
+ */
+ public interface Listener {
/**
* Called when a subtitle track has been selected.
*
diff --git a/media2/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaItemTest.java b/media2/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaItemTest.java
index 8a44064..b217031 100644
--- a/media2/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaItemTest.java
+++ b/media2/version-compat-tests/current/service/src/androidTest/java/androidx/media2/test/service/tests/MediaItemTest.java
@@ -83,7 +83,7 @@
public MediaItem create(Context context) {
final MediaMetadata testMetadata = new MediaMetadata.Builder()
.putString("MediaItemTest", "MediaItemTest").build();
- return new UriMediaItem.Builder(context, Uri.parse("test://test"))
+ return new UriMediaItem.Builder(Uri.parse("test://test"))
.setMetadata(testMetadata)
.setStartPosition(1)
.setEndPosition(1000)
diff --git a/mediarouter/build.gradle b/mediarouter/build.gradle
index 4381eae..98b4ae4 100644
--- a/mediarouter/build.gradle
+++ b/mediarouter/build.gradle
@@ -10,7 +10,7 @@
api(project(":media"))
implementation("androidx.appcompat:appcompat:1.0.2")
implementation("androidx.palette:palette:1.0.0")
- implementation(project(":recyclerview"))
+ implementation("androidx.recyclerview:recyclerview:1.0.0")
androidTestImplementation(TEST_EXT_JUNIT)
androidTestImplementation(TEST_CORE)
diff --git a/navigation/common/api/2.1.0-alpha02.txt b/navigation/common/api/2.1.0-alpha02.txt
new file mode 100644
index 0000000..6dd3420
--- /dev/null
+++ b/navigation/common/api/2.1.0-alpha02.txt
@@ -0,0 +1,192 @@
+// Signature format: 3.0
+package androidx.navigation {
+
+ public final class ActionOnlyNavDirections implements androidx.navigation.NavDirections {
+ ctor public ActionOnlyNavDirections(int);
+ method public int getActionId();
+ method public android.os.Bundle getArguments();
+ }
+
+ public final class NavAction {
+ ctor public NavAction(@IdRes int);
+ ctor public NavAction(@IdRes int, androidx.navigation.NavOptions?);
+ ctor public NavAction(@IdRes int, androidx.navigation.NavOptions?, android.os.Bundle?);
+ method public android.os.Bundle? getDefaultArguments();
+ method public int getDestinationId();
+ method public androidx.navigation.NavOptions? getNavOptions();
+ method public void setDefaultArguments(android.os.Bundle?);
+ method public void setNavOptions(androidx.navigation.NavOptions?);
+ }
+
+ public interface NavArgs {
+ }
+
+ public final class NavArgument {
+ method public Object? getDefaultValue();
+ method public androidx.navigation.NavType<?> getType();
+ method public boolean isDefaultValuePresent();
+ method public boolean isNullable();
+ }
+
+ public static final class NavArgument.Builder {
+ ctor public NavArgument.Builder();
+ method public androidx.navigation.NavArgument build();
+ method public androidx.navigation.NavArgument.Builder setDefaultValue(Object?);
+ method public androidx.navigation.NavArgument.Builder setIsNullable(boolean);
+ method public androidx.navigation.NavArgument.Builder setType(androidx.navigation.NavType<?>);
+ }
+
+ public class NavDestination {
+ ctor public NavDestination(androidx.navigation.Navigator<? extends androidx.navigation.NavDestination>);
+ ctor public NavDestination(String);
+ method public final void addArgument(String, androidx.navigation.NavArgument);
+ method public final void addDeepLink(String);
+ method public final androidx.navigation.NavAction? getAction(@IdRes int);
+ method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
+ method @IdRes public final int getId();
+ method public final CharSequence? getLabel();
+ method public final String getNavigatorName();
+ method public final androidx.navigation.NavGraph? getParent();
+ method @CallSuper public void onInflate(android.content.Context, android.util.AttributeSet);
+ method protected static <C> Class<? extends C> parseClassFromName(android.content.Context, String, Class<? extends C>);
+ method public final void putAction(@IdRes int, @IdRes int);
+ method public final void putAction(@IdRes int, androidx.navigation.NavAction);
+ method public final void removeAction(@IdRes int);
+ method public final void removeArgument(String);
+ method public final void setId(@IdRes int);
+ method public final void setLabel(CharSequence?);
+ }
+
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE}) public static @interface NavDestination.ClassType {
+ method public abstract Class value();
+ }
+
+ public interface NavDirections {
+ method @IdRes public int getActionId();
+ method public android.os.Bundle getArguments();
+ }
+
+ public class NavGraph extends androidx.navigation.NavDestination implements java.lang.Iterable<androidx.navigation.NavDestination> {
+ ctor public NavGraph(androidx.navigation.Navigator<? extends androidx.navigation.NavGraph>);
+ method public final void addAll(androidx.navigation.NavGraph);
+ method public final void addDestination(androidx.navigation.NavDestination);
+ method public final void addDestinations(java.util.Collection<androidx.navigation.NavDestination>);
+ method public final void addDestinations(androidx.navigation.NavDestination...);
+ method public final void clear();
+ method public final androidx.navigation.NavDestination? findNode(@IdRes int);
+ method @IdRes public final int getStartDestination();
+ method public final java.util.Iterator<androidx.navigation.NavDestination> iterator();
+ method public final void remove(androidx.navigation.NavDestination);
+ method public final void setStartDestination(@IdRes int);
+ }
+
+ @androidx.navigation.Navigator.Name("navigation") public class NavGraphNavigator extends androidx.navigation.Navigator<androidx.navigation.NavGraph> {
+ ctor public NavGraphNavigator(androidx.navigation.NavigatorProvider);
+ method public androidx.navigation.NavGraph createDestination();
+ method public androidx.navigation.NavDestination? navigate(androidx.navigation.NavGraph, android.os.Bundle?, androidx.navigation.NavOptions?, androidx.navigation.Navigator.Extras?);
+ method public boolean popBackStack();
+ }
+
+ public final class NavOptions {
+ method @AnimRes @AnimatorRes public int getEnterAnim();
+ method @AnimRes @AnimatorRes public int getExitAnim();
+ method @AnimRes @AnimatorRes public int getPopEnterAnim();
+ method @AnimRes @AnimatorRes public int getPopExitAnim();
+ method @IdRes public int getPopUpTo();
+ method public boolean isPopUpToInclusive();
+ method public boolean shouldLaunchSingleTop();
+ }
+
+ public static final class NavOptions.Builder {
+ ctor public NavOptions.Builder();
+ method public androidx.navigation.NavOptions build();
+ method public androidx.navigation.NavOptions.Builder setEnterAnim(@AnimRes @AnimatorRes int);
+ method public androidx.navigation.NavOptions.Builder setExitAnim(@AnimRes @AnimatorRes int);
+ method public androidx.navigation.NavOptions.Builder setLaunchSingleTop(boolean);
+ method public androidx.navigation.NavOptions.Builder setPopEnterAnim(@AnimRes @AnimatorRes int);
+ method public androidx.navigation.NavOptions.Builder setPopExitAnim(@AnimRes @AnimatorRes int);
+ method public androidx.navigation.NavOptions.Builder setPopUpTo(@IdRes int, boolean);
+ }
+
+ public abstract class NavType<T> {
+ method public static androidx.navigation.NavType<?> fromArgType(String?, String?);
+ method public abstract T? get(android.os.Bundle, String);
+ method public abstract String getName();
+ method public boolean isNullableAllowed();
+ method public abstract T parseValue(String);
+ method public abstract void put(android.os.Bundle, String, T?);
+ field public static final androidx.navigation.NavType<boolean[]> BoolArrayType;
+ field public static final androidx.navigation.NavType<java.lang.Boolean> BoolType;
+ field public static final androidx.navigation.NavType<float[]> FloatArrayType;
+ field public static final androidx.navigation.NavType<java.lang.Float> FloatType;
+ field public static final androidx.navigation.NavType<int[]> IntArrayType;
+ field public static final androidx.navigation.NavType<java.lang.Integer> IntType;
+ field public static final androidx.navigation.NavType<long[]> LongArrayType;
+ field public static final androidx.navigation.NavType<java.lang.Long> LongType;
+ field public static final androidx.navigation.NavType<java.lang.Integer> ReferenceType;
+ field public static final androidx.navigation.NavType<java.lang.String[]> StringArrayType;
+ field public static final androidx.navigation.NavType<java.lang.String> StringType;
+ }
+
+ public static final class NavType.EnumType<D extends java.lang.Enum> extends androidx.navigation.NavType.SerializableType<D> {
+ ctor public NavType.EnumType(Class<D>);
+ }
+
+ public static final class NavType.ParcelableArrayType<D extends android.os.Parcelable> extends androidx.navigation.NavType<D[]> {
+ ctor public NavType.ParcelableArrayType(Class<D>);
+ method public D[]? get(android.os.Bundle, String);
+ method public String getName();
+ method public D[] parseValue(String);
+ method public void put(android.os.Bundle, String, D[]?);
+ }
+
+ public static final class NavType.ParcelableType<D> extends androidx.navigation.NavType<D> {
+ ctor public NavType.ParcelableType(Class<D>);
+ method public D? get(android.os.Bundle, String);
+ method public String getName();
+ method public D parseValue(String);
+ method public void put(android.os.Bundle, String, D?);
+ }
+
+ public static final class NavType.SerializableArrayType<D extends java.io.Serializable> extends androidx.navigation.NavType<D[]> {
+ ctor public NavType.SerializableArrayType(Class<D>);
+ method public D[]? get(android.os.Bundle, String);
+ method public String getName();
+ method public D[] parseValue(String);
+ method public void put(android.os.Bundle, String, D[]?);
+ }
+
+ public static class NavType.SerializableType<D extends java.io.Serializable> extends androidx.navigation.NavType<D> {
+ ctor public NavType.SerializableType(Class<D>);
+ method public D? get(android.os.Bundle, String);
+ method public String getName();
+ method public D parseValue(String);
+ method public void put(android.os.Bundle, String, D?);
+ }
+
+ public abstract class Navigator<D extends androidx.navigation.NavDestination> {
+ ctor public Navigator();
+ method public abstract D createDestination();
+ method public abstract androidx.navigation.NavDestination? navigate(D, android.os.Bundle?, androidx.navigation.NavOptions?, androidx.navigation.Navigator.Extras?);
+ method public void onRestoreState(android.os.Bundle);
+ method public android.os.Bundle? onSaveState();
+ method public abstract boolean popBackStack();
+ }
+
+ public static interface Navigator.Extras {
+ }
+
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE}) public static @interface Navigator.Name {
+ method public abstract String value();
+ }
+
+ public class NavigatorProvider {
+ ctor public NavigatorProvider();
+ method public final androidx.navigation.Navigator<? extends androidx.navigation.NavDestination>? addNavigator(androidx.navigation.Navigator<? extends androidx.navigation.NavDestination>);
+ method @CallSuper public androidx.navigation.Navigator<? extends androidx.navigation.NavDestination>? addNavigator(String, androidx.navigation.Navigator<? extends androidx.navigation.NavDestination>);
+ method public final <T extends androidx.navigation.Navigator<?>> T getNavigator(Class<T>);
+ method @CallSuper public <T extends androidx.navigation.Navigator<?>> T getNavigator(String);
+ }
+
+}
+
diff --git a/navigation/common/api/res-2.1.0-alpha02.txt b/navigation/common/api/res-2.1.0-alpha02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/common/api/res-2.1.0-alpha02.txt
diff --git a/navigation/common/api/restricted_2.1.0-alpha02.txt b/navigation/common/api/restricted_2.1.0-alpha02.txt
new file mode 100644
index 0000000..37d70c2
--- /dev/null
+++ b/navigation/common/api/restricted_2.1.0-alpha02.txt
@@ -0,0 +1,17 @@
+// Signature format: 3.0
+package androidx.navigation {
+
+ public abstract class Navigator<D extends androidx.navigation.NavDestination> {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void addOnNavigatorBackPressListener(androidx.navigation.Navigator.OnNavigatorBackPressListener);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void dispatchOnNavigatorBackPress();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) protected void onBackPressAdded();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) protected void onBackPressRemoved();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public final void removeOnNavigatorBackPressListener(androidx.navigation.Navigator.OnNavigatorBackPressListener);
+ }
+
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static interface Navigator.OnNavigatorBackPressListener {
+ method public void onPopBackStack(androidx.navigation.Navigator);
+ }
+
+}
+
diff --git a/navigation/common/ktx/api/2.1.0-alpha02.txt b/navigation/common/ktx/api/2.1.0-alpha02.txt
new file mode 100644
index 0000000..6da57db
--- /dev/null
+++ b/navigation/common/ktx/api/2.1.0-alpha02.txt
@@ -0,0 +1,129 @@
+// Signature format: 3.0
+package androidx.navigation {
+
+ @androidx.navigation.NavOptionsDsl public final class AnimBuilder {
+ ctor public AnimBuilder();
+ method public int getEnter();
+ method public int getExit();
+ method public int getPopEnter();
+ method public int getPopExit();
+ method public void setEnter(int p);
+ method public void setExit(int p);
+ method public void setPopEnter(int p);
+ method public void setPopExit(int p);
+ property public final int enter;
+ property public final int exit;
+ property public final int popEnter;
+ property public final int popExit;
+ }
+
+ @androidx.navigation.NavDestinationDsl public final class NavActionBuilder {
+ ctor public NavActionBuilder();
+ method public int getDestinationId();
+ method public void navOptions(kotlin.jvm.functions.Function1<? super androidx.navigation.NavOptionsBuilder,kotlin.Unit> optionsBuilder);
+ method public void setDestinationId(int p);
+ property public final int destinationId;
+ }
+
+ public final class NavArgsLazy<Args extends androidx.navigation.NavArgs> implements kotlin.Lazy<Args> {
+ ctor public NavArgsLazy(kotlin.reflect.KClass<Args> navArgsClass, kotlin.jvm.functions.Function0<android.os.Bundle> argumentProducer);
+ method public Args getValue();
+ method public boolean isInitialized();
+ property public Args value;
+ }
+
+ public final class NavArgsLazyKt {
+ ctor public NavArgsLazyKt();
+ }
+
+ @androidx.navigation.NavDestinationDsl public final class NavArgumentBuilder {
+ ctor public NavArgumentBuilder();
+ method public androidx.navigation.NavArgument build();
+ method public Object? getDefaultValue();
+ method public boolean getNullable();
+ method public androidx.navigation.NavType<?> getType();
+ method public void setDefaultValue(Object? value);
+ method public void setNullable(boolean value);
+ method public void setType(androidx.navigation.NavType<?> value);
+ property public final Object? defaultValue;
+ property public final boolean nullable;
+ property public final androidx.navigation.NavType<?> type;
+ }
+
+ @androidx.navigation.NavDestinationDsl public class NavDestinationBuilder<D extends androidx.navigation.NavDestination> {
+ ctor public NavDestinationBuilder(androidx.navigation.Navigator<? extends D> navigator, @IdRes int id);
+ method public final void action(int actionId, kotlin.jvm.functions.Function1<? super androidx.navigation.NavActionBuilder,kotlin.Unit> actionBuilder);
+ method public final void argument(String name, kotlin.jvm.functions.Function1<? super androidx.navigation.NavArgumentBuilder,kotlin.Unit> argumentBuilder);
+ method public D build();
+ method public final void deepLink(String uriPattern);
+ method public final int getId();
+ method public final CharSequence? getLabel();
+ method protected final androidx.navigation.Navigator<? extends D> getNavigator();
+ method public final void setLabel(CharSequence? p);
+ property public final CharSequence? label;
+ }
+
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public @interface NavDestinationDsl {
+ }
+
+ @androidx.navigation.NavDestinationDsl public final class NavGraphBuilder extends androidx.navigation.NavDestinationBuilder<androidx.navigation.NavGraph> {
+ ctor public NavGraphBuilder(androidx.navigation.NavigatorProvider provider, @IdRes int id, @IdRes int startDestination);
+ method public void addDestination(androidx.navigation.NavDestination destination);
+ method public androidx.navigation.NavGraph build();
+ method public <D extends androidx.navigation.NavDestination> void destination(androidx.navigation.NavDestinationBuilder<? extends D> navDestination);
+ method public androidx.navigation.NavigatorProvider getProvider();
+ method public operator void unaryPlus(androidx.navigation.NavDestination);
+ }
+
+ public final class NavGraphBuilderKt {
+ ctor public NavGraphBuilderKt();
+ method public static inline androidx.navigation.NavGraph navigation(androidx.navigation.NavigatorProvider, @IdRes int id = 0, @IdRes int startDestination, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
+ method public static inline void navigation(androidx.navigation.NavGraphBuilder, @IdRes int id, @IdRes int startDestination, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
+ }
+
+ public final class NavGraphKt {
+ ctor public NavGraphKt();
+ method public static operator boolean contains(androidx.navigation.NavGraph, @IdRes int id);
+ method public static inline operator androidx.navigation.NavDestination get(androidx.navigation.NavGraph, @IdRes int id);
+ method public static inline operator void minusAssign(androidx.navigation.NavGraph, androidx.navigation.NavDestination node);
+ method public static inline operator void plusAssign(androidx.navigation.NavGraph, androidx.navigation.NavDestination node);
+ method public static inline operator void plusAssign(androidx.navigation.NavGraph, androidx.navigation.NavGraph other);
+ }
+
+ @androidx.navigation.NavOptionsDsl public final class NavOptionsBuilder {
+ ctor public NavOptionsBuilder();
+ method public void anim(kotlin.jvm.functions.Function1<? super androidx.navigation.AnimBuilder,kotlin.Unit> animBuilder);
+ method public boolean getLaunchSingleTop();
+ method public int getPopUpTo();
+ method public void popUpTo(@IdRes int id, kotlin.jvm.functions.Function1<? super androidx.navigation.PopUpToBuilder,kotlin.Unit> popUpToBuilder);
+ method public void setLaunchSingleTop(boolean p);
+ method public void setPopUpTo(int value);
+ property public final boolean launchSingleTop;
+ property public final int popUpTo;
+ }
+
+ public final class NavOptionsBuilderKt {
+ ctor public NavOptionsBuilderKt();
+ method public static androidx.navigation.NavOptions navOptions(kotlin.jvm.functions.Function1<? super androidx.navigation.NavOptionsBuilder,kotlin.Unit> optionsBuilder);
+ }
+
+ @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public @interface NavOptionsDsl {
+ }
+
+ public final class NavigatorProviderKt {
+ ctor public NavigatorProviderKt();
+ method public static inline operator <T extends androidx.navigation.Navigator<? extends androidx.navigation.NavDestination>> T get(androidx.navigation.NavigatorProvider, String name);
+ method public static inline operator <T extends androidx.navigation.Navigator<? extends androidx.navigation.NavDestination>> T get(androidx.navigation.NavigatorProvider, kotlin.reflect.KClass<T> clazz);
+ method public static inline operator void plusAssign(androidx.navigation.NavigatorProvider, androidx.navigation.Navigator<? extends androidx.navigation.NavDestination> navigator);
+ method public static inline operator androidx.navigation.Navigator<? extends androidx.navigation.NavDestination>? set(androidx.navigation.NavigatorProvider, String name, androidx.navigation.Navigator<? extends androidx.navigation.NavDestination> navigator);
+ }
+
+ @androidx.navigation.NavOptionsDsl public final class PopUpToBuilder {
+ ctor public PopUpToBuilder();
+ method public boolean getInclusive();
+ method public void setInclusive(boolean p);
+ property public final boolean inclusive;
+ }
+
+}
+
diff --git a/navigation/common/ktx/api/res-2.1.0-alpha02.txt b/navigation/common/ktx/api/res-2.1.0-alpha02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/common/ktx/api/res-2.1.0-alpha02.txt
diff --git a/navigation/common/ktx/api/restricted_2.1.0-alpha02.txt b/navigation/common/ktx/api/restricted_2.1.0-alpha02.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/navigation/common/ktx/api/restricted_2.1.0-alpha02.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/navigation/fragment/api/2.1.0-alpha02.txt b/navigation/fragment/api/2.1.0-alpha02.txt
new file mode 100644
index 0000000..8cd8b69
--- /dev/null
+++ b/navigation/fragment/api/2.1.0-alpha02.txt
@@ -0,0 +1,40 @@
+// Signature format: 3.0
+package androidx.navigation.fragment {
+
+ @androidx.navigation.Navigator.Name("fragment") public class FragmentNavigator extends androidx.navigation.Navigator<androidx.navigation.fragment.FragmentNavigator.Destination> {
+ ctor public FragmentNavigator(android.content.Context, androidx.fragment.app.FragmentManager, int);
+ method public androidx.navigation.fragment.FragmentNavigator.Destination createDestination();
+ method @Deprecated public androidx.fragment.app.Fragment instantiateFragment(android.content.Context, androidx.fragment.app.FragmentManager, String, android.os.Bundle?);
+ method public androidx.navigation.NavDestination? navigate(androidx.navigation.fragment.FragmentNavigator.Destination, android.os.Bundle?, androidx.navigation.NavOptions?, androidx.navigation.Navigator.Extras?);
+ method public boolean popBackStack();
+ }
+
+ @androidx.navigation.NavDestination.ClassType(Fragment.class) public static class FragmentNavigator.Destination extends androidx.navigation.NavDestination {
+ ctor public FragmentNavigator.Destination(androidx.navigation.NavigatorProvider);
+ ctor public FragmentNavigator.Destination(androidx.navigation.Navigator<? extends androidx.navigation.fragment.FragmentNavigator.Destination>);
+ method public final String getClassName();
+ method public final androidx.navigation.fragment.FragmentNavigator.Destination setClassName(String);
+ }
+
+ public static final class FragmentNavigator.Extras implements androidx.navigation.Navigator.Extras {
+ method public java.util.Map<android.view.View,java.lang.String> getSharedElements();
+ }
+
+ public static final class FragmentNavigator.Extras.Builder {
+ ctor public FragmentNavigator.Extras.Builder();
+ method public androidx.navigation.fragment.FragmentNavigator.Extras.Builder addSharedElement(android.view.View, String);
+ method public androidx.navigation.fragment.FragmentNavigator.Extras.Builder addSharedElements(java.util.Map<android.view.View,java.lang.String>);
+ method public androidx.navigation.fragment.FragmentNavigator.Extras build();
+ }
+
+ public class NavHostFragment extends androidx.fragment.app.Fragment implements androidx.navigation.NavHost {
+ ctor public NavHostFragment();
+ method public static androidx.navigation.fragment.NavHostFragment create(@NavigationRes int);
+ method public static androidx.navigation.fragment.NavHostFragment create(@NavigationRes int, android.os.Bundle?);
+ method protected androidx.navigation.Navigator<? extends androidx.navigation.fragment.FragmentNavigator.Destination> createFragmentNavigator();
+ method public static androidx.navigation.NavController findNavController(androidx.fragment.app.Fragment);
+ method public final androidx.navigation.NavController getNavController();
+ }
+
+}
+
diff --git a/navigation/fragment/api/res-2.1.0-alpha02.txt b/navigation/fragment/api/res-2.1.0-alpha02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/fragment/api/res-2.1.0-alpha02.txt
diff --git a/navigation/fragment/api/restricted_2.1.0-alpha02.txt b/navigation/fragment/api/restricted_2.1.0-alpha02.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/navigation/fragment/api/restricted_2.1.0-alpha02.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/navigation/fragment/build.gradle b/navigation/fragment/build.gradle
index fdcf9f7..6e279e7 100644
--- a/navigation/fragment/build.gradle
+++ b/navigation/fragment/build.gradle
@@ -31,7 +31,7 @@
}
dependencies {
- api("androidx.fragment:fragment:1.1.0-alpha05")
+ api(project(":fragment"))
api(project(":navigation:navigation-runtime"))
testImplementation(JUNIT)
diff --git a/navigation/fragment/ktx/api/2.1.0-alpha02.txt b/navigation/fragment/ktx/api/2.1.0-alpha02.txt
new file mode 100644
index 0000000..b8b9aba
--- /dev/null
+++ b/navigation/fragment/ktx/api/2.1.0-alpha02.txt
@@ -0,0 +1,31 @@
+// Signature format: 3.0
+package androidx.navigation.fragment {
+
+ public final class FragmentKt {
+ ctor public FragmentKt();
+ method public static androidx.navigation.NavController findNavController(androidx.fragment.app.Fragment);
+ }
+
+ public final class FragmentNavArgsLazyKt {
+ ctor public FragmentNavArgsLazyKt();
+ method @MainThread public static inline <reified Args extends androidx.navigation.NavArgs> androidx.navigation.NavArgsLazy<Args>! navArgs(androidx.fragment.app.Fragment);
+ }
+
+ public final class FragmentNavigatorDestinationBuilder extends androidx.navigation.NavDestinationBuilder<androidx.navigation.fragment.FragmentNavigator.Destination> {
+ ctor public FragmentNavigatorDestinationBuilder(androidx.navigation.fragment.FragmentNavigator navigator, @IdRes int id, kotlin.reflect.KClass<? extends androidx.fragment.app.Fragment> fragmentClass);
+ method public androidx.navigation.fragment.FragmentNavigator.Destination build();
+ }
+
+ public final class FragmentNavigatorDestinationBuilderKt {
+ ctor public FragmentNavigatorDestinationBuilderKt();
+ method public static inline <reified F extends androidx.fragment.app.Fragment> void fragment(androidx.navigation.NavGraphBuilder, @IdRes int id);
+ method public static inline <reified F extends androidx.fragment.app.Fragment> void fragment(androidx.navigation.NavGraphBuilder, @IdRes int id, kotlin.jvm.functions.Function1<? super androidx.navigation.fragment.FragmentNavigatorDestinationBuilder,kotlin.Unit>! builder);
+ }
+
+ public final class FragmentNavigatorExtrasKt {
+ ctor public FragmentNavigatorExtrasKt();
+ method public static androidx.navigation.fragment.FragmentNavigator.Extras FragmentNavigatorExtras(kotlin.Pair<? extends android.view.View,java.lang.String>... sharedElements);
+ }
+
+}
+
diff --git a/navigation/fragment/ktx/api/res-2.1.0-alpha02.txt b/navigation/fragment/ktx/api/res-2.1.0-alpha02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/fragment/ktx/api/res-2.1.0-alpha02.txt
diff --git a/navigation/fragment/ktx/api/restricted_2.1.0-alpha02.txt b/navigation/fragment/ktx/api/restricted_2.1.0-alpha02.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/navigation/fragment/ktx/api/restricted_2.1.0-alpha02.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/navigation/runtime/api/2.1.0-alpha02.txt b/navigation/runtime/api/2.1.0-alpha02.txt
new file mode 100644
index 0000000..f34f225
--- /dev/null
+++ b/navigation/runtime/api/2.1.0-alpha02.txt
@@ -0,0 +1,103 @@
+// Signature format: 3.0
+package androidx.navigation {
+
+ @androidx.navigation.Navigator.Name("activity") public class ActivityNavigator extends androidx.navigation.Navigator<androidx.navigation.ActivityNavigator.Destination> {
+ ctor public ActivityNavigator(android.content.Context);
+ method public static void applyPopAnimationsToPendingTransition(android.app.Activity);
+ method public androidx.navigation.ActivityNavigator.Destination createDestination();
+ method public androidx.navigation.NavDestination? navigate(androidx.navigation.ActivityNavigator.Destination, android.os.Bundle?, androidx.navigation.NavOptions?, androidx.navigation.Navigator.Extras?);
+ method public boolean popBackStack();
+ }
+
+ @androidx.navigation.NavDestination.ClassType(Activity.class) public static class ActivityNavigator.Destination extends androidx.navigation.NavDestination {
+ ctor public ActivityNavigator.Destination(androidx.navigation.NavigatorProvider);
+ ctor public ActivityNavigator.Destination(androidx.navigation.Navigator<? extends androidx.navigation.ActivityNavigator.Destination>);
+ method public final String? getAction();
+ method public final android.content.ComponentName? getComponent();
+ method public final android.net.Uri? getData();
+ method public final String? getDataPattern();
+ method public final android.content.Intent? getIntent();
+ method public final androidx.navigation.ActivityNavigator.Destination setAction(String?);
+ method public final androidx.navigation.ActivityNavigator.Destination setComponentName(android.content.ComponentName?);
+ method public final androidx.navigation.ActivityNavigator.Destination setData(android.net.Uri?);
+ method public final androidx.navigation.ActivityNavigator.Destination setDataPattern(String?);
+ method public final androidx.navigation.ActivityNavigator.Destination setIntent(android.content.Intent?);
+ }
+
+ public static final class ActivityNavigator.Extras implements androidx.navigation.Navigator.Extras {
+ method public androidx.core.app.ActivityOptionsCompat? getActivityOptions();
+ method public int getFlags();
+ }
+
+ public static final class ActivityNavigator.Extras.Builder {
+ ctor public ActivityNavigator.Extras.Builder();
+ method public androidx.navigation.ActivityNavigator.Extras.Builder addFlags(int);
+ method public androidx.navigation.ActivityNavigator.Extras build();
+ method public androidx.navigation.ActivityNavigator.Extras.Builder setActivityOptions(androidx.core.app.ActivityOptionsCompat);
+ }
+
+ public class NavController {
+ ctor public NavController(android.content.Context);
+ method public void addOnDestinationChangedListener(androidx.navigation.NavController.OnDestinationChangedListener);
+ method public androidx.navigation.NavDeepLinkBuilder createDeepLink();
+ method public androidx.navigation.NavDestination? getCurrentDestination();
+ method public androidx.navigation.NavGraph getGraph();
+ method public androidx.navigation.NavInflater getNavInflater();
+ method public androidx.navigation.NavigatorProvider getNavigatorProvider();
+ method public boolean handleDeepLink(android.content.Intent?);
+ method public void navigate(@IdRes int);
+ method public void navigate(@IdRes int, android.os.Bundle?);
+ method public void navigate(@IdRes int, android.os.Bundle?, androidx.navigation.NavOptions?);
+ method public void navigate(@IdRes int, android.os.Bundle?, androidx.navigation.NavOptions?, androidx.navigation.Navigator.Extras?);
+ method public void navigate(androidx.navigation.NavDirections);
+ method public void navigate(androidx.navigation.NavDirections, androidx.navigation.NavOptions?);
+ method public void navigate(androidx.navigation.NavDirections, androidx.navigation.Navigator.Extras);
+ method public boolean navigateUp();
+ method public boolean popBackStack();
+ method public boolean popBackStack(@IdRes int, boolean);
+ method public void removeOnDestinationChangedListener(androidx.navigation.NavController.OnDestinationChangedListener);
+ method @CallSuper public void restoreState(android.os.Bundle?);
+ method @CallSuper public android.os.Bundle? saveState();
+ method @CallSuper public void setGraph(@NavigationRes int);
+ method @CallSuper public void setGraph(@NavigationRes int, android.os.Bundle?);
+ method @CallSuper public void setGraph(androidx.navigation.NavGraph);
+ method @CallSuper public void setGraph(androidx.navigation.NavGraph, android.os.Bundle?);
+ field public static final String KEY_DEEP_LINK_INTENT = "android-support-nav:controller:deepLinkIntent";
+ }
+
+ public static interface NavController.OnDestinationChangedListener {
+ method public void onDestinationChanged(androidx.navigation.NavController, androidx.navigation.NavDestination, android.os.Bundle?);
+ }
+
+ public final class NavDeepLinkBuilder {
+ ctor public NavDeepLinkBuilder(android.content.Context);
+ method public android.app.PendingIntent createPendingIntent();
+ method public androidx.core.app.TaskStackBuilder createTaskStackBuilder();
+ method public androidx.navigation.NavDeepLinkBuilder setArguments(android.os.Bundle?);
+ method public androidx.navigation.NavDeepLinkBuilder setComponentName(Class<? extends android.app.Activity>);
+ method public androidx.navigation.NavDeepLinkBuilder setComponentName(android.content.ComponentName);
+ method public androidx.navigation.NavDeepLinkBuilder setDestination(@IdRes int);
+ method public androidx.navigation.NavDeepLinkBuilder setGraph(@NavigationRes int);
+ method public androidx.navigation.NavDeepLinkBuilder setGraph(androidx.navigation.NavGraph);
+ }
+
+ public interface NavHost {
+ method public androidx.navigation.NavController getNavController();
+ }
+
+ public final class NavInflater {
+ ctor public NavInflater(android.content.Context, androidx.navigation.NavigatorProvider);
+ method public androidx.navigation.NavGraph inflate(@NavigationRes int);
+ }
+
+ public final class Navigation {
+ method public static android.view.View.OnClickListener createNavigateOnClickListener(@IdRes int);
+ method public static android.view.View.OnClickListener createNavigateOnClickListener(@IdRes int, android.os.Bundle?);
+ method public static android.view.View.OnClickListener createNavigateOnClickListener(androidx.navigation.NavDirections);
+ method public static androidx.navigation.NavController findNavController(android.app.Activity, @IdRes int);
+ method public static androidx.navigation.NavController findNavController(android.view.View);
+ method public static void setViewNavController(android.view.View, androidx.navigation.NavController?);
+ }
+
+}
+
diff --git a/navigation/runtime/api/res-2.1.0-alpha02.txt b/navigation/runtime/api/res-2.1.0-alpha02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/runtime/api/res-2.1.0-alpha02.txt
diff --git a/navigation/runtime/api/restricted_2.1.0-alpha02.txt b/navigation/runtime/api/restricted_2.1.0-alpha02.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/navigation/runtime/api/restricted_2.1.0-alpha02.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/navigation/runtime/ktx/api/2.1.0-alpha02.txt b/navigation/runtime/ktx/api/2.1.0-alpha02.txt
new file mode 100644
index 0000000..2967c7a
--- /dev/null
+++ b/navigation/runtime/ktx/api/2.1.0-alpha02.txt
@@ -0,0 +1,57 @@
+// Signature format: 3.0
+package androidx.navigation {
+
+ public final class ActivityKt {
+ ctor public ActivityKt();
+ method public static androidx.navigation.NavController findNavController(android.app.Activity, @IdRes int viewId);
+ }
+
+ public final class ActivityNavArgsLazyKt {
+ ctor public ActivityNavArgsLazyKt();
+ method @MainThread public static inline <reified Args extends androidx.navigation.NavArgs> androidx.navigation.NavArgsLazy<Args>! navArgs(android.app.Activity);
+ }
+
+ public final class ActivityNavigatorDestinationBuilder extends androidx.navigation.NavDestinationBuilder<androidx.navigation.ActivityNavigator.Destination> {
+ ctor public ActivityNavigatorDestinationBuilder(androidx.navigation.ActivityNavigator navigator, @IdRes int id);
+ method public androidx.navigation.ActivityNavigator.Destination build();
+ method public String? getAction();
+ method public kotlin.reflect.KClass<? extends android.app.Activity>? getActivityClass();
+ method public android.net.Uri? getData();
+ method public String? getDataPattern();
+ method public void setAction(String? p);
+ method public void setActivityClass(kotlin.reflect.KClass<? extends android.app.Activity>? p);
+ method public void setData(android.net.Uri? p);
+ method public void setDataPattern(String? p);
+ property public final String? action;
+ property public final kotlin.reflect.KClass<? extends android.app.Activity>? activityClass;
+ property public final android.net.Uri? data;
+ property public final String? dataPattern;
+ }
+
+ public final class ActivityNavigatorDestinationBuilderKt {
+ ctor public ActivityNavigatorDestinationBuilderKt();
+ method public static inline void activity(androidx.navigation.NavGraphBuilder, @IdRes int id, kotlin.jvm.functions.Function1<? super androidx.navigation.ActivityNavigatorDestinationBuilder,kotlin.Unit> builder);
+ }
+
+ public final class ActivityNavigatorExtrasKt {
+ ctor public ActivityNavigatorExtrasKt();
+ method public static androidx.navigation.ActivityNavigator.Extras ActivityNavigatorExtras(androidx.core.app.ActivityOptionsCompat? activityOptions = null, int flags = 0);
+ }
+
+ public final class NavControllerKt {
+ ctor public NavControllerKt();
+ method public static inline androidx.navigation.NavGraph createGraph(androidx.navigation.NavController, @IdRes int id = 0, @IdRes int startDestination, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
+ }
+
+ public final class NavHostKt {
+ ctor public NavHostKt();
+ method public static inline androidx.navigation.NavGraph createGraph(androidx.navigation.NavHost, @IdRes int id = 0, @IdRes int startDestination, kotlin.jvm.functions.Function1<? super androidx.navigation.NavGraphBuilder,kotlin.Unit> builder);
+ }
+
+ public final class ViewKt {
+ ctor public ViewKt();
+ method public static androidx.navigation.NavController findNavController(android.view.View);
+ }
+
+}
+
diff --git a/navigation/runtime/ktx/api/res-2.1.0-alpha02.txt b/navigation/runtime/ktx/api/res-2.1.0-alpha02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/runtime/ktx/api/res-2.1.0-alpha02.txt
diff --git a/navigation/runtime/ktx/api/restricted_2.1.0-alpha02.txt b/navigation/runtime/ktx/api/restricted_2.1.0-alpha02.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/navigation/runtime/ktx/api/restricted_2.1.0-alpha02.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/navigation/ui/api/2.1.0-alpha02.txt b/navigation/ui/api/2.1.0-alpha02.txt
new file mode 100644
index 0000000..629226a
--- /dev/null
+++ b/navigation/ui/api/2.1.0-alpha02.txt
@@ -0,0 +1,42 @@
+// Signature format: 3.0
+package androidx.navigation.ui {
+
+ public final class AppBarConfiguration {
+ method public androidx.drawerlayout.widget.DrawerLayout? getDrawerLayout();
+ method public androidx.navigation.ui.AppBarConfiguration.OnNavigateUpListener? getFallbackOnNavigateUpListener();
+ method public java.util.Set<java.lang.Integer> getTopLevelDestinations();
+ }
+
+ public static final class AppBarConfiguration.Builder {
+ ctor public AppBarConfiguration.Builder(androidx.navigation.NavGraph);
+ ctor public AppBarConfiguration.Builder(android.view.Menu);
+ ctor public AppBarConfiguration.Builder(int...);
+ ctor public AppBarConfiguration.Builder(java.util.Set<java.lang.Integer>);
+ method public androidx.navigation.ui.AppBarConfiguration build();
+ method public androidx.navigation.ui.AppBarConfiguration.Builder setDrawerLayout(androidx.drawerlayout.widget.DrawerLayout?);
+ method public androidx.navigation.ui.AppBarConfiguration.Builder setFallbackOnNavigateUpListener(androidx.navigation.ui.AppBarConfiguration.OnNavigateUpListener?);
+ }
+
+ public static interface AppBarConfiguration.OnNavigateUpListener {
+ method public boolean onNavigateUp();
+ }
+
+ public final class NavigationUI {
+ method public static boolean navigateUp(androidx.navigation.NavController, androidx.drawerlayout.widget.DrawerLayout?);
+ method public static boolean navigateUp(androidx.navigation.NavController, androidx.navigation.ui.AppBarConfiguration);
+ method public static boolean onNavDestinationSelected(android.view.MenuItem, androidx.navigation.NavController);
+ method public static void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity, androidx.navigation.NavController);
+ method public static void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity, androidx.navigation.NavController, androidx.drawerlayout.widget.DrawerLayout?);
+ method public static void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity, androidx.navigation.NavController, androidx.navigation.ui.AppBarConfiguration);
+ method public static void setupWithNavController(androidx.appcompat.widget.Toolbar, androidx.navigation.NavController);
+ method public static void setupWithNavController(androidx.appcompat.widget.Toolbar, androidx.navigation.NavController, androidx.drawerlayout.widget.DrawerLayout?);
+ method public static void setupWithNavController(androidx.appcompat.widget.Toolbar, androidx.navigation.NavController, androidx.navigation.ui.AppBarConfiguration);
+ method public static void setupWithNavController(com.google.android.material.appbar.CollapsingToolbarLayout, androidx.appcompat.widget.Toolbar, androidx.navigation.NavController);
+ method public static void setupWithNavController(com.google.android.material.appbar.CollapsingToolbarLayout, androidx.appcompat.widget.Toolbar, androidx.navigation.NavController, androidx.drawerlayout.widget.DrawerLayout?);
+ method public static void setupWithNavController(com.google.android.material.appbar.CollapsingToolbarLayout, androidx.appcompat.widget.Toolbar, androidx.navigation.NavController, androidx.navigation.ui.AppBarConfiguration);
+ method public static void setupWithNavController(com.google.android.material.navigation.NavigationView, androidx.navigation.NavController);
+ method public static void setupWithNavController(com.google.android.material.bottomnavigation.BottomNavigationView, androidx.navigation.NavController);
+ }
+
+}
+
diff --git a/navigation/ui/api/res-2.1.0-alpha02.txt b/navigation/ui/api/res-2.1.0-alpha02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/ui/api/res-2.1.0-alpha02.txt
diff --git a/navigation/ui/api/restricted_2.1.0-alpha02.txt b/navigation/ui/api/restricted_2.1.0-alpha02.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/navigation/ui/api/restricted_2.1.0-alpha02.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/navigation/ui/ktx/api/2.1.0-alpha02.txt b/navigation/ui/ktx/api/2.1.0-alpha02.txt
new file mode 100644
index 0000000..d20eb8f
--- /dev/null
+++ b/navigation/ui/ktx/api/2.1.0-alpha02.txt
@@ -0,0 +1,51 @@
+// Signature format: 3.0
+package androidx.navigation.ui {
+
+ public final class ActivityKt {
+ ctor public ActivityKt();
+ method public static void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity, androidx.navigation.NavController navController, androidx.drawerlayout.widget.DrawerLayout? drawerLayout);
+ method public static void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity, androidx.navigation.NavController navController, androidx.navigation.ui.AppBarConfiguration configuration = AppBarConfiguration(navController.graph));
+ }
+
+ public final class AppBarConfigurationKt {
+ ctor public AppBarConfigurationKt();
+ method public static inline androidx.navigation.ui.AppBarConfiguration AppBarConfiguration(androidx.navigation.NavGraph navGraph, androidx.drawerlayout.widget.DrawerLayout? drawerLayout = null, kotlin.jvm.functions.Function0<java.lang.Boolean> fallbackOnNavigateUpListener = { false });
+ method public static inline androidx.navigation.ui.AppBarConfiguration AppBarConfiguration(android.view.Menu topLevelMenu, androidx.drawerlayout.widget.DrawerLayout? drawerLayout = null, kotlin.jvm.functions.Function0<java.lang.Boolean> fallbackOnNavigateUpListener = { false });
+ method public static inline androidx.navigation.ui.AppBarConfiguration AppBarConfiguration(java.util.Set<java.lang.Integer> topLevelDestinationIds, androidx.drawerlayout.widget.DrawerLayout? drawerLayout = null, kotlin.jvm.functions.Function0<java.lang.Boolean> fallbackOnNavigateUpListener = { false });
+ }
+
+ public final class BottomNavigationViewKt {
+ ctor public BottomNavigationViewKt();
+ method public static void setupWithNavController(com.google.android.material.bottomnavigation.BottomNavigationView, androidx.navigation.NavController navController);
+ }
+
+ public final class CollapsingToolbarLayoutKt {
+ ctor public CollapsingToolbarLayoutKt();
+ method public static void setupWithNavController(com.google.android.material.appbar.CollapsingToolbarLayout, androidx.appcompat.widget.Toolbar toolbar, androidx.navigation.NavController navController, androidx.drawerlayout.widget.DrawerLayout? drawerLayout);
+ method public static void setupWithNavController(com.google.android.material.appbar.CollapsingToolbarLayout, androidx.appcompat.widget.Toolbar toolbar, androidx.navigation.NavController navController, androidx.navigation.ui.AppBarConfiguration configuration = AppBarConfiguration(navController.graph));
+ }
+
+ public final class MenuItemKt {
+ ctor public MenuItemKt();
+ method public static boolean onNavDestinationSelected(android.view.MenuItem, androidx.navigation.NavController navController);
+ }
+
+ public final class NavControllerKt {
+ ctor public NavControllerKt();
+ method public static boolean navigateUp(androidx.navigation.NavController, androidx.drawerlayout.widget.DrawerLayout? drawerLayout);
+ method public static boolean navigateUp(androidx.navigation.NavController, androidx.navigation.ui.AppBarConfiguration appBarConfiguration);
+ }
+
+ public final class NavigationViewKt {
+ ctor public NavigationViewKt();
+ method public static void setupWithNavController(com.google.android.material.navigation.NavigationView, androidx.navigation.NavController navController);
+ }
+
+ public final class ToolbarKt {
+ ctor public ToolbarKt();
+ method public static void setupWithNavController(androidx.appcompat.widget.Toolbar, androidx.navigation.NavController navController, androidx.drawerlayout.widget.DrawerLayout? drawerLayout);
+ method public static void setupWithNavController(androidx.appcompat.widget.Toolbar, androidx.navigation.NavController navController, androidx.navigation.ui.AppBarConfiguration configuration = AppBarConfiguration(navController.graph));
+ }
+
+}
+
diff --git a/navigation/ui/ktx/api/res-2.1.0-alpha02.txt b/navigation/ui/ktx/api/res-2.1.0-alpha02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/ui/ktx/api/res-2.1.0-alpha02.txt
diff --git a/navigation/ui/ktx/api/restricted_2.1.0-alpha02.txt b/navigation/ui/ktx/api/restricted_2.1.0-alpha02.txt
new file mode 100644
index 0000000..da4f6cc
--- /dev/null
+++ b/navigation/ui/ktx/api/restricted_2.1.0-alpha02.txt
@@ -0,0 +1 @@
+// Signature format: 3.0
diff --git a/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java b/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
index 26af8f8..2efc8f3 100644
--- a/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
+++ b/preference/src/main/java/androidx/preference/PreferenceFragmentCompat.java
@@ -604,8 +604,11 @@
} else if (preference instanceof MultiSelectListPreference) {
f = MultiSelectListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
} else {
- throw new IllegalArgumentException("Tried to display dialog for unknown " +
- "preference type. Did you forget to override onDisplayPreferenceDialog()?");
+ throw new IllegalArgumentException(
+ "Cannot display dialog for an unknown Preference type: "
+ + preference.getClass().getSimpleName()
+ + ". Make sure to implement onPreferenceDisplayDialog() to handle "
+ + "displaying a custom dialog for this Preference.");
}
f.setTargetFragment(this, 0);
f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index f8e522e..9163de3 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -1314,20 +1314,20 @@
ensureLayoutState();
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
- final int absDy = Math.abs(delta);
- updateLayoutState(layoutDirection, absDy, true, state);
+ final int absDelta = Math.abs(delta);
+ updateLayoutState(layoutDirection, absDelta, true, state);
collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
}
- int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
- if (getChildCount() == 0 || dy == 0) {
+ int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (getChildCount() == 0 || delta == 0) {
return 0;
}
ensureLayoutState();
mLayoutState.mRecycle = true;
- final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
- final int absDy = Math.abs(dy);
- updateLayoutState(layoutDirection, absDy, true, state);
+ final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
+ final int absDelta = Math.abs(delta);
+ updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
@@ -1336,10 +1336,10 @@
}
return 0;
}
- final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
+ final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
- Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
+ Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
@@ -1979,7 +1979,6 @@
return null;
}
ensureLayoutState();
- ensureLayoutState();
final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace());
updateLayoutState(layoutDir, maxScroll, false, state);
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
diff --git a/room/common-java8/build.gradle b/room/common-java8/build.gradle
new file mode 100644
index 0000000..4bd7f60
--- /dev/null
+++ b/room/common-java8/build.gradle
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension;
+
+plugins {
+ id("SupportJavaLibraryPlugin")
+}
+
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
+
+dependencies {
+ compile(ANDROIDX_ANNOTATION)
+}
+
+supportLibrary {
+ name = "Android Room-Common-Java8"
+ publish = false
+ mavenVersion = LibraryVersions.ROOM
+ mavenGroup = LibraryGroups.ROOM
+ inceptionYear = "2019"
+ description = "Android Room-Common-Java8"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
\ No newline at end of file
diff --git a/room/common-java8/src/main/java/androidx/room/util/SneakyThrow.java b/room/common-java8/src/main/java/androidx/room/util/SneakyThrow.java
new file mode 100644
index 0000000..21497d2
--- /dev/null
+++ b/room/common-java8/src/main/java/androidx/room/util/SneakyThrow.java
@@ -0,0 +1,47 @@
+/*
+ * 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.room.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Java 8 Sneaky Throw technique.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class SneakyThrow {
+
+ /**
+ * Re-throws a checked exception as if it was a runtime exception without wrapping it.
+ *
+ * @param e the exception to re-throw.
+ */
+ public static void reThrow(@NonNull Exception e) {
+ sneakyThrow(e);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <E extends Throwable> void sneakyThrow(@NonNull Throwable e) throws E {
+ throw (E) e;
+ }
+
+ private SneakyThrow() {
+
+ }
+}
diff --git a/room/compiler/SQLite.g4 b/room/compiler/SQLite.g4
index 8eb895d..ab60202 100644
--- a/room/compiler/SQLite.g4
+++ b/room/compiler/SQLite.g4
@@ -28,16 +28,16 @@
* https://github.com/bkiers/sqlite-parser
* Developed by : Bart Kiers, [email protected]
*/
-grammar SQLite;
+grammar SQLite; // For version 3.24.0 of SQLite
parse
: ( sql_stmt_list | error )* EOF
;
error
- : UNEXPECTED_CHAR
- {
- throw new RuntimeException("UNEXPECTED_CHAR=" + $UNEXPECTED_CHAR.text);
+ : UNEXPECTED_CHAR
+ {
+ throw new RuntimeException("UNEXPECTED_CHAR=" + $UNEXPECTED_CHAR.text);
}
;
@@ -51,7 +51,6 @@
| attach_stmt
| begin_stmt
| commit_stmt
- | compound_select_stmt
| create_index_stmt
| create_table_stmt
| create_trigger_stmt
@@ -64,14 +63,12 @@
| drop_table_stmt
| drop_trigger_stmt
| drop_view_stmt
- | factored_select_stmt
| insert_stmt
| pragma_stmt
| reindex_stmt
| release_stmt
| rollback_stmt
| savepoint_stmt
- | simple_select_stmt
| select_stmt
| update_stmt
| update_stmt_limited
@@ -79,18 +76,18 @@
;
alter_table_stmt
- : K_ALTER K_TABLE ( database_name '.' )? table_name
+ : K_ALTER K_TABLE ( schema_name '.' )? table_name
( K_RENAME K_TO new_table_name
| K_ADD K_COLUMN? column_def
)
;
analyze_stmt
- : K_ANALYZE ( database_name | table_or_index_name | database_name '.' table_or_index_name )?
+ : K_ANALYZE ( schema_name | table_or_index_name | schema_name '.' table_or_index_name )?
;
attach_stmt
- : K_ATTACH K_DATABASE? expr K_AS database_name
+ : K_ATTACH K_DATABASE? expr K_AS schema_name
;
begin_stmt
@@ -101,43 +98,36 @@
: ( K_COMMIT | K_END ) ( K_TRANSACTION transaction_name? )?
;
-compound_select_stmt
- : with_clause?
- select_core ( ( K_UNION K_ALL? | K_INTERSECT | K_EXCEPT ) select_core )+
- ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- ( K_LIMIT expr ( ( K_OFFSET | ',' ) expr )? )?
- ;
-
create_index_stmt
: K_CREATE K_UNIQUE? K_INDEX ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? index_name K_ON table_name '(' indexed_column ( ',' indexed_column )* ')'
+ ( schema_name '.' )? index_name K_ON table_name '(' indexed_column ( ',' indexed_column )* ')'
( K_WHERE expr )?
;
create_table_stmt
: K_CREATE ( K_TEMP | K_TEMPORARY )? K_TABLE ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? table_name
- ( '(' column_def ( ',' column_def )*? ( ',' table_constraint )* ')' ( K_WITHOUT IDENTIFIER )?
+ ( schema_name '.' )? table_name
+ ( '(' column_def ( ',' column_def )*? ( ',' table_constraint )* ')' WITHOUT_ROWID?
| K_AS select_stmt
)
;
create_trigger_stmt
: K_CREATE ( K_TEMP | K_TEMPORARY )? K_TRIGGER ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? trigger_name ( K_BEFORE | K_AFTER | K_INSTEAD K_OF )?
- ( K_DELETE | K_INSERT | K_UPDATE ( K_OF column_name ( ',' column_name )* )? ) K_ON ( database_name '.' )? table_name
+ ( schema_name '.' )? trigger_name ( K_BEFORE | K_AFTER | K_INSTEAD K_OF )?
+ ( K_DELETE | K_INSERT | K_UPDATE ( K_OF column_name ( ',' column_name )* )? ) K_ON ( schema_name '.' )? table_name
( K_FOR K_EACH K_ROW )? ( K_WHEN expr )?
K_BEGIN ( ( update_stmt | insert_stmt | delete_stmt | select_stmt ) ';' )+ K_END
;
create_view_stmt
: K_CREATE ( K_TEMP | K_TEMPORARY )? K_VIEW ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? view_name K_AS select_stmt
+ ( schema_name '.' )? view_name ( column_name ( ',' column_name )* )? K_AS select_stmt
;
create_virtual_table_stmt
: K_CREATE K_VIRTUAL K_TABLE ( K_IF K_NOT K_EXISTS )?
- ( database_name '.' )? table_name
+ ( schema_name '.' )? table_name
K_USING module_name ( '(' module_argument ( ',' module_argument )* ')' )?
;
@@ -149,36 +139,27 @@
delete_stmt_limited
: with_clause? K_DELETE K_FROM qualified_table_name
( K_WHERE expr )?
- ( ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- K_LIMIT expr ( ( K_OFFSET | ',' ) expr )?
- )?
+ ( order_clause? limit_clause )?
;
detach_stmt
- : K_DETACH K_DATABASE? database_name
+ : K_DETACH K_DATABASE? schema_name
;
drop_index_stmt
- : K_DROP K_INDEX ( K_IF K_EXISTS )? ( database_name '.' )? index_name
+ : K_DROP K_INDEX ( K_IF K_EXISTS )? ( schema_name '.' )? index_name
;
drop_table_stmt
- : K_DROP K_TABLE ( K_IF K_EXISTS )? ( database_name '.' )? table_name
+ : K_DROP K_TABLE ( K_IF K_EXISTS )? ( schema_name '.' )? table_name
;
drop_trigger_stmt
- : K_DROP K_TRIGGER ( K_IF K_EXISTS )? ( database_name '.' )? trigger_name
+ : K_DROP K_TRIGGER ( K_IF K_EXISTS )? ( schema_name '.' )? trigger_name
;
drop_view_stmt
- : K_DROP K_VIEW ( K_IF K_EXISTS )? ( database_name '.' )? view_name
- ;
-
-factored_select_stmt
- : with_clause?
- select_core ( compound_operator select_core )*
- ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- ( K_LIMIT expr ( ( K_OFFSET | ',' ) expr )? )?
+ : K_DROP K_VIEW ( K_IF K_EXISTS )? ( schema_name '.' )? view_name
;
insert_stmt
@@ -189,21 +170,30 @@
| K_INSERT K_OR K_ABORT
| K_INSERT K_OR K_FAIL
| K_INSERT K_OR K_IGNORE ) K_INTO
- ( database_name '.' )? table_name ( '(' column_name ( ',' column_name )* ')' )?
+ ( schema_name '.' )? table_name ( K_AS table_alias )? ( '(' column_name ( ',' column_name )* ')' )?
( K_VALUES '(' expr ( ',' expr )* ')' ( ',' '(' expr ( ',' expr )* ')' )*
| select_stmt
| K_DEFAULT K_VALUES
)
+ upsert_clause?
+ ;
+
+upsert_clause
+ : K_ON K_CONFLICT ( '(' indexed_column ( ',' indexed_column )* ')' ( K_WHERE expr )? )?
+ ( DO_NOTHING
+ | DO_UPDATE K_SET ( column_name | column_name_list ) '=' expr
+ ( ',' ( column_name | column_name_list ) '=' expr )*
+ ( K_WHERE expr )?
+ )
;
pragma_stmt
- : K_PRAGMA ( database_name '.' )? pragma_name ( '=' pragma_value
- | '(' pragma_value ')' )?
+ : K_PRAGMA ( schema_name '.' )? pragma_name ( '=' pragma_value | '(' pragma_value ')' )?
;
reindex_stmt
: K_REINDEX ( collation_name
- | ( database_name '.' )? ( table_name | index_name )
+ | ( schema_name '.' )? ( table_name | index_name )
)?
;
@@ -219,17 +209,11 @@
: K_SAVEPOINT savepoint_name
;
-simple_select_stmt
- : with_clause?
- select_core ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- ( K_LIMIT expr ( ( K_OFFSET | ',' ) expr )? )?
- ;
-
select_stmt
: with_clause?
select_or_values ( compound_operator select_or_values )*
- ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- ( K_LIMIT expr ( ( K_OFFSET | ',' ) expr )? )?
+ order_clause?
+ limit_clause?
;
select_or_values
@@ -246,7 +230,8 @@
| K_OR K_REPLACE
| K_OR K_FAIL
| K_OR K_IGNORE )? qualified_table_name
- K_SET column_name '=' expr ( ',' column_name '=' expr )* ( K_WHERE expr )?
+ K_SET ( column_name | column_name_list ) '=' expr ( ',' ( column_name | column_name_list ) '=' expr )*
+ ( K_WHERE expr )?
;
update_stmt_limited
@@ -255,14 +240,13 @@
| K_OR K_REPLACE
| K_OR K_FAIL
| K_OR K_IGNORE )? qualified_table_name
- K_SET column_name '=' expr ( ',' column_name '=' expr )* ( K_WHERE expr )?
- ( ( K_ORDER K_BY ordering_term ( ',' ordering_term )* )?
- K_LIMIT expr ( ( K_OFFSET | ',' ) expr )?
- )?
+ K_SET ( column_name | column_name_list ) '=' expr ( ',' ( column_name | column_name_list ) '=' expr )*
+ ( K_WHERE expr )?
+ ( order_clause? limit_clause )?
;
vacuum_stmt
- : K_VACUUM
+ : K_VACUUM schema_name?
;
column_def
@@ -271,7 +255,7 @@
type_name
: name+? ( '(' signed_number ')'
- | '(' signed_number ',' signed_number ')' )?
+ | '(' signed_number ',' signed_number ')' )?
;
column_constraint
@@ -296,45 +280,23 @@
)?
;
-/*
- SQLite understands the following binary operators, in order from highest to
- lowest precedence:
-
- ||
- * / %
- + -
- << >> & |
- < <= > >=
- = == != <> IS IS NOT IN LIKE GLOB MATCH REGEXP
- AND
- OR
-*/
expr
: literal_value
| BIND_PARAMETER
- | ( ( database_name '.' )? table_name '.' )? column_name
+ | ( ( schema_name '.' )? table_name '.' )? column_name
| unary_operator expr
- | expr '||' expr
- | expr ( '*' | '/' | '%' ) expr
- | expr ( '+' | '-' ) expr
- | expr ( '<<' | '>>' | '&' | '|' ) expr
- | expr ( '<' | '<=' | '>' | '>=' ) expr
- | expr ( '=' | '==' | '!=' | '<>' ) expr
- | expr K_AND expr
- | expr K_OR expr
+ | expr binary_operator expr
| function_name '(' ( K_DISTINCT? expr ( ',' expr )* | '*' )? ')'
- | '(' expr ')'
+ | '(' expr ( ',' expr )* ')'
| K_CAST '(' expr K_AS type_name ')'
| expr K_COLLATE collation_name
| expr K_NOT? ( K_LIKE | K_GLOB | K_REGEXP | K_MATCH ) expr ( K_ESCAPE expr )?
| expr ( K_ISNULL | K_NOTNULL | K_NOT K_NULL )
| expr K_IS K_NOT? expr
| expr K_NOT? K_BETWEEN expr K_AND expr
- | expr K_NOT? K_IN ( '(' ( select_stmt
- | expr ( ',' expr )*
- )?
- ')'
- | ( database_name '.' )? table_name )
+ | expr K_NOT? K_IN ( '(' ( select_stmt | expr ( ',' expr )* )? ')'
+ | ( schema_name '.' )? table_name
+ | ( schema_name '.' )? table_function '(' ( expr ( ',' expr )* )? ')' )
| ( ( K_NOT )? K_EXISTS )? '(' select_stmt ')'
| K_CASE expr? ( K_WHEN expr K_THEN expr )+ ( K_ELSE expr )? K_END
| raise_function
@@ -360,7 +322,7 @@
;
indexed_column
- : column_name ( K_COLLATE collation_name )? ( K_ASC | K_DESC )?
+ : ( column_name | expr ) ( K_COLLATE collation_name )? ( K_ASC | K_DESC )?
;
table_constraint
@@ -375,23 +337,32 @@
: K_WITH K_RECURSIVE? common_table_expression ( ',' common_table_expression )*
;
+common_table_expression
+ : table_name ( '(' column_name ( ',' column_name )* ')' )? K_AS '(' select_stmt ')'
+ ;
+
qualified_table_name
- : ( database_name '.' )? table_name ( K_INDEXED K_BY index_name
- | K_NOT K_INDEXED )?
+ : ( schema_name '.' )? table_name ( K_AS table_alias )?
+ ( K_INDEXED K_BY index_name | K_NOT K_INDEXED )?
+ ;
+
+order_clause
+ : K_ORDER K_BY ordering_term ( ',' ordering_term )*
;
ordering_term
: expr ( K_COLLATE collation_name )? ( K_ASC | K_DESC )?
;
+limit_clause
+ : K_LIMIT expr ( ( K_OFFSET | ',' ) expr )?
+ ;
+
pragma_value
: signed_number
| name
| STRING_LITERAL
- ;
-
-common_table_expression
- : table_name ( '(' column_name ( ',' column_name )* ')' )? K_AS '(' select_stmt ')'
+ | boolean_literal
;
result_column
@@ -402,12 +373,9 @@
table_or_subquery
: ( schema_name '.' )? table_name ( K_AS? table_alias )?
- ( K_INDEXED K_BY index_name
- | K_NOT K_INDEXED )?
- | ( schema_name '.' )? table_function_name '(' ( expr ( ',' expr )* )? ')' ( K_AS? table_alias )?
- | '(' ( table_or_subquery ( ',' table_or_subquery )*
- | join_clause )
- ')'
+ ( K_INDEXED K_BY index_name | K_NOT K_INDEXED )?
+ | ( schema_name '.' )? table_function '(' ( expr ( ',' expr )* )? ')' ( K_AS? table_alias )?
+ | '(' ( table_or_subquery ( ',' table_or_subquery )* | join_clause ) ')'
| '(' select_stmt ')' ( K_AS? table_alias )?
;
@@ -425,14 +393,6 @@
| K_USING '(' column_name ( ',' column_name )* ')' )?
;
-select_core
- : K_SELECT ( K_DISTINCT | K_ALL )? result_column ( ',' result_column )*
- ( K_FROM ( table_or_subquery ( ',' table_or_subquery )* | join_clause ) )?
- ( K_WHERE expr )?
- ( K_GROUP K_BY expr ( ',' expr )* ( K_HAVING expr )? )?
- | K_VALUES '(' expr ( ',' expr )* ')' ( ',' '(' expr ( ',' expr )* ')' )*
- ;
-
compound_operator
: K_UNION
| K_UNION K_ALL
@@ -452,6 +412,12 @@
| K_CURRENT_TIME
| K_CURRENT_DATE
| K_CURRENT_TIMESTAMP
+ | boolean_literal
+ ;
+
+boolean_literal
+ : TRUE
+ | FALSE
;
unary_operator
@@ -461,6 +427,33 @@
| K_NOT
;
+/*
+ SQLite understands the following binary operators, in order from highest to
+ lowest precedence:
+
+ ||
+ * / %
+ + -
+ << >> & |
+ < <= > >=
+ = == != <> IS IS NOT IN LIKE GLOB MATCH REGEXP
+ AND
+ OR
+
+ This rule is only used in `expr`, which has more complete directives for `IS` through `REGEXP`,
+ so we leave them out here.
+*/
+binary_operator
+ : '||'
+ | ( '*' | '/' | '%' )
+ | ( '+' | '-' )
+ | ( '<<' | '>>' | '&' | '|' )
+ | ( '<' | '<=' | '>' | '>=' )
+ | ( '=' | '==' | '!=' | '<>' )
+ | K_AND
+ | K_OR
+ ;
+
error_message
: STRING_LITERAL
;
@@ -475,6 +468,10 @@
| STRING_LITERAL
;
+column_name_list
+ : '(' column_name ( ',' column_name )* ')'
+ ;
+
keyword
: K_ABORT
| K_ACTION
@@ -612,15 +609,11 @@
: any_name
;
-database_name
- : any_name
- ;
-
schema_name
: any_name
;
-table_function_name
+table_function
: any_name
;
@@ -713,6 +706,8 @@
EQ : '==';
NOT_EQ1 : '!=';
NOT_EQ2 : '<>';
+TRUE : T R U E;
+FALSE : F A L S E;
// http://www.sqlite.org/lang_keywords.html
K_ABORT : A B O R T;
@@ -840,16 +835,22 @@
K_WITH : W I T H;
K_WITHOUT : W I T H O U T;
+// These are not keywords, but their constituents might be wrongly matched as identifiers.
+WITHOUT_ROWID: K_WITHOUT SPACES R O W I D;
+DO_NOTHING: D O SPACES N O T H I N G;
+DO_UPDATE: D O SPACES K_UPDATE;
+
IDENTIFIER
: '"' (~'"' | '""')* '"'
| '`' (~'`' | '``')* '`'
| '[' ~']'* ']'
- | [a-zA-Z_] [a-zA-Z_0-9]* // TODO check: needs more chars in set
+ | [a-zA-Z_\u00a1-\uffff] [a-zA-Z_0-9\u00a1-\uffff]*
;
NUMERIC_LITERAL
: DIGIT+ ( '.' DIGIT* )? ( E [-+]? DIGIT+ )?
| '.' DIGIT+ ( E [-+]? DIGIT+ )?
+ | '0' X HEXDIGIT+
;
BIND_PARAMETER
@@ -882,6 +883,7 @@
;
fragment DIGIT : [0-9];
+fragment HEXDIGIT : [0-9a-fA-F];
fragment A : [aA];
fragment B : [bB];
diff --git a/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt b/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
index dba9aa7..bb93c23 100644
--- a/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
@@ -51,16 +51,11 @@
private fun findQueryType(statement: ParseTree): QueryType {
return when (statement) {
- is SQLiteParser.Factored_select_stmtContext,
- is SQLiteParser.Compound_select_stmtContext,
- is SQLiteParser.Select_stmtContext,
- is SQLiteParser.Simple_select_stmtContext ->
+ is SQLiteParser.Select_stmtContext ->
QueryType.SELECT
-
is SQLiteParser.Delete_stmt_limitedContext,
is SQLiteParser.Delete_stmtContext ->
QueryType.DELETE
-
is SQLiteParser.Insert_stmtContext ->
QueryType.INSERT
is SQLiteParser.Update_stmtContext,
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index 8dc5742..4d8cfa7 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -201,9 +201,10 @@
adapter = context.typeAdapterStore.findPreparedQueryResultAdapter(returnType, query)
) { callableImpl, dbField ->
addStatement(
- "return $T.execute($N, $L, $N)",
+ "return $T.execute($N, $L, $L, $N)",
RoomCoroutinesTypeNames.COROUTINES_ROOM,
dbField,
+ "true", // inTransaction
callableImpl,
continuationParam.simpleName.toString()
)
@@ -217,9 +218,10 @@
adapter = context.typeAdapterStore.findInsertAdapter(returnType, params)
) { callableImpl, dbField ->
addStatement(
- "return $T.execute($N, $L, $N)",
+ "return $T.execute($N, $L, $L, $N)",
RoomCoroutinesTypeNames.COROUTINES_ROOM,
dbField,
+ "true", // inTransaction
callableImpl,
continuationParam.simpleName.toString()
)
@@ -231,9 +233,10 @@
adapter = context.typeAdapterStore.findDeleteOrUpdateAdapter(returnType)
) { callableImpl, dbField ->
addStatement(
- "return $T.execute($N, $L, $N)",
+ "return $T.execute($N, $L, $L, $N)",
RoomCoroutinesTypeNames.COROUTINES_ROOM,
dbField,
+ "true", // inTransaction
callableImpl,
continuationParam.simpleName.toString()
)
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/prepared/binderprovider/GuavaListenableFuturePreparedQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/prepared/binderprovider/GuavaListenableFuturePreparedQueryResultBinderProvider.kt
index 95e4e71..54b3d23 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/prepared/binderprovider/GuavaListenableFuturePreparedQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/prepared/binderprovider/GuavaListenableFuturePreparedQueryResultBinderProvider.kt
@@ -52,9 +52,10 @@
adapter = context.typeAdapterStore.findPreparedQueryResultAdapter(typeArg, query)
) { callableImpl, dbField ->
addStatement(
- "return $T.createListenableFuture($N, $L)",
+ "return $T.createListenableFuture($N, $L, $L)",
RoomGuavaTypeNames.GUAVA_ROOM,
dbField,
+ "true", // inTransaction
callableImpl
)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
index b59ecc0..d97ba61 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/CoroutineResultBinder.kt
@@ -57,9 +57,10 @@
scope.builder().apply {
addStatement(
- "return $T.execute($N, $L, $N)",
+ "return $T.execute($N, $L, $L, $N)",
RoomCoroutinesTypeNames.COROUTINES_ROOM,
dbField,
+ if (inTransaction) "true" else "false",
callableImpl,
continuationParamName)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
index 7141786..722242e 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaListenableFutureQueryResultBinder.kt
@@ -56,9 +56,10 @@
scope.builder().apply {
addStatement(
- "return $T.createListenableFuture($N, $L, $L, $L)",
+ "return $T.createListenableFuture($N, $L, $L, $L, $L)",
RoomGuavaTypeNames.GUAVA_ROOM,
dbField,
+ if (inTransaction) "true" else "false",
callableImpl,
roomSQLiteQueryVar,
canReleaseQuery
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
index 2eebcda5..cd84015 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -67,8 +67,12 @@
scope.builder().apply {
val tableNamesList = tableNames.joinToString(",") { "\"$it\"" }
addStatement(
- "return $N.getInvalidationTracker().createLiveData(new $T{$L}, $L)",
- dbField, String::class.arrayTypeName(), tableNamesList, callableImpl
+ "return $N.getInvalidationTracker().createLiveData(new $T{$L}, $L, $L)",
+ dbField,
+ String::class.arrayTypeName(),
+ tableNamesList,
+ if (inTransaction) "true" else "false",
+ callableImpl
)
}
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
index 7b2e54b..d52e804 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/query/result/RxQueryResultBinder.kt
@@ -58,9 +58,14 @@
}.build()
scope.builder().apply {
val tableNamesList = queryTableNames.joinToString(",") { "\"$it\"" }
- addStatement("return $T.$N($N, new $T{$L}, $L)",
- RoomRxJava2TypeNames.RX_ROOM, rxType.methodName, dbField,
- String::class.arrayTypeName(), tableNamesList, callableImpl)
+ addStatement("return $T.$N($N, $L, new $T{$L}, $L)",
+ RoomRxJava2TypeNames.RX_ROOM,
+ rxType.methodName,
+ dbField,
+ if (inTransaction) "true" else "false",
+ String::class.arrayTypeName(),
+ tableNamesList,
+ callableImpl)
}
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureDeleteOrUpdateMethodBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureDeleteOrUpdateMethodBinderProvider.kt
index b43e23c..106bc14 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureDeleteOrUpdateMethodBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureDeleteOrUpdateMethodBinderProvider.kt
@@ -54,9 +54,10 @@
val adapter = context.typeAdapterStore.findDeleteOrUpdateAdapter(typeArg)
return createDeleteOrUpdateBinder(typeArg, adapter) { callableImpl, dbField ->
addStatement(
- "return $T.createListenableFuture($N, $L)",
+ "return $T.createListenableFuture($N, $L, $L)",
RoomGuavaTypeNames.GUAVA_ROOM,
dbField,
+ "true", // inTransaction
callableImpl
)
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureInsertMethodBinderProvider.kt b/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureInsertMethodBinderProvider.kt
index 7b3cb47..f603510 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureInsertMethodBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureInsertMethodBinderProvider.kt
@@ -58,9 +58,10 @@
val adapter = context.typeAdapterStore.findInsertAdapter(typeArg, params)
return createInsertBinder(typeArg, adapter) { callableImpl, dbField ->
addStatement(
- "return $T.createListenableFuture($N, $L)",
+ "return $T.createListenableFuture($N, $L, $L)",
RoomGuavaTypeNames.GUAVA_ROOM,
dbField,
+ "true", // inTransaction
callableImpl
)
}
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index bb945a3..f047ecd4 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -281,7 +281,7 @@
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
int _argIndex = 1;
_statement.bindLong(_argIndex, id);
- return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, new Callable<User>() {
+ return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, false, new Callable<User>() {
@Override
public User call() throws Exception {
final Cursor _cursor = DBUtil.query(__db, _statement, false);
@@ -330,7 +330,7 @@
_statement.bindLong(_argIndex, _item);
_argIndex ++;
}
- return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, new Callable<List<User>>() {
+ return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, false, new Callable<List<User>>() {
@Override
public List<User> call() throws Exception {
final Cursor _cursor = DBUtil.query(__db, _statement, false);
diff --git a/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
index 885f7bd..f939e26 100644
--- a/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/parser/SqlParserTest.kt
@@ -66,6 +66,17 @@
}
@Test
+ fun upsertQuery() {
+ val parsed = SqlParser.parse(
+ "INSERT INTO notes (id, content) VALUES (:id, :content) " +
+ "ON CONFLICT (id) DO UPDATE SET content = excluded.content, " +
+ "revision = revision + 1, modifiedTime = strftime('%s','now')"
+ )
+ assertThat(parsed.errors, `is`(emptyList()))
+ assertThat(parsed.type, `is`(QueryType.INSERT))
+ }
+
+ @Test
fun explain() {
assertErrors("EXPLAIN QUERY PLAN SELECT * FROM users",
ParserErrors.invalidQueryType(QueryType.EXPLAIN))
@@ -140,6 +151,19 @@
}
@Test
+ fun unicodeInIdentifiers() {
+ val query = SqlParser.parse("SELECT 名, 色 FROM 猫")
+ assertThat(query.errors, `is`(emptyList()))
+ assertThat(query.tables, `is`(setOf(Table("猫", "猫"))))
+ }
+
+ @Test
+ fun rowValue_where() {
+ val query = SqlParser.parse("SELECT * FROM notes WHERE (id, content) > (:id, :content)")
+ assertThat(query.errors, `is`(emptyList()))
+ }
+
+ @Test
fun findBindVariables() {
assertVariables("select * from users")
assertVariables("select * from users where name like ?", "?")
diff --git a/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java b/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java
index d4f6c2f..4e2f500 100644
--- a/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java
+++ b/room/guava/src/main/java/androidx/room/guava/GuavaRoom.java
@@ -48,8 +48,8 @@
* Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
* {@link ArchTaskExecutor}'s background-threaded Executor.
*
- * @deprecated
- * Use {@link #createListenableFuture(RoomDatabase, Callable, RoomSQLiteQuery, boolean)}
+ * @deprecated Use {@link #createListenableFuture(RoomDatabase, boolean, Callable,
+ * RoomSQLiteQuery, boolean)}
*/
@Deprecated
public static <T> ListenableFuture<T> createListenableFuture(
@@ -63,7 +63,11 @@
/**
* Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
* {@link RoomDatabase}'s {@link java.util.concurrent.Executor}.
+ *
+ * @deprecated Use {@link #createListenableFuture(RoomDatabase, boolean, Callable,
+ * RoomSQLiteQuery, boolean)}
*/
+ @Deprecated
public static <T> ListenableFuture<T> createListenableFuture(
final RoomDatabase roomDatabase,
final Callable<T> callable,
@@ -73,6 +77,20 @@
roomDatabase.getQueryExecutor(), callable, query, releaseQuery);
}
+ /**
+ * Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
+ * {@link RoomDatabase}'s {@link java.util.concurrent.Executor}.
+ */
+ public static <T> ListenableFuture<T> createListenableFuture(
+ final RoomDatabase roomDatabase,
+ final boolean inTransaction,
+ final Callable<T> callable,
+ final RoomSQLiteQuery query,
+ final boolean releaseQuery) {
+ return createListenableFuture(
+ getExecutor(roomDatabase, inTransaction), callable, query, releaseQuery);
+ }
+
private static <T> ListenableFuture<T> createListenableFuture(
final Executor executor,
final Callable<T> callable,
@@ -104,12 +122,34 @@
/**
* Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
* {@link RoomDatabase}'s {@link java.util.concurrent.Executor}.
+ *
+ * @deprecated Use {@link #createListenableFuture(RoomDatabase, boolean, Callable)}
*/
+ @Deprecated
public static <T> ListenableFuture<T> createListenableFuture(
final RoomDatabase roomDatabase,
final Callable<T> callable) {
+ return createListenableFuture(roomDatabase, false, callable);
+ }
+
+ /**
+ * Returns a {@link ListenableFuture<T>} created by submitting the input {@code callable} to
+ * {@link RoomDatabase}'s {@link java.util.concurrent.Executor}.
+ */
+ public static <T> ListenableFuture<T> createListenableFuture(
+ final RoomDatabase roomDatabase,
+ final boolean inTransaction,
+ final Callable<T> callable) {
ListenableFutureTask<T> listenableFutureTask = ListenableFutureTask.create(callable);
- roomDatabase.getQueryExecutor().execute(listenableFutureTask);
+ getExecutor(roomDatabase, inTransaction).execute(listenableFutureTask);
return listenableFutureTask;
}
+
+ private static Executor getExecutor(RoomDatabase database, boolean inTransaction) {
+ if (inTransaction) {
+ return database.getTransactionExecutor();
+ } else {
+ return database.getQueryExecutor();
+ }
+ }
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SneakyThrowTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SneakyThrowTest.kt
new file mode 100644
index 0000000..f462d00
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SneakyThrowTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.room.integration.kotlintestapp.test
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.json.JSONException
+import org.json.JSONObject
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.Callable
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SneakyThrowTest : TestDatabaseTest() {
+
+ @Test
+ fun testCheckedException() {
+ try {
+ database.runInTransaction(Callable<String> {
+ val json = JSONObject()
+ json.getString("key") // method declares that it throws JSONException
+ })
+ fail("runInTransaction should have thrown an exception")
+ } catch (ex: JSONException) {
+ // no-op on purpose
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index 901f410..b8e81fa 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -17,6 +17,7 @@
package androidx.room.integration.kotlintestapp.test
import android.os.Build
+import androidx.arch.core.executor.ArchTaskExecutor
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.integration.kotlintestapp.NewThreadDispatcher
@@ -45,8 +46,10 @@
import org.junit.runner.RunWith
import java.io.IOException
import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -633,6 +636,52 @@
@Test
@Suppress("DeferredResultUnused")
+ fun withTransaction_multipleTransactions_verifyThreadUsage() {
+ val busyThreadsCount = AtomicInteger()
+ // Executor wrapper that counts threads that are busy executing commands.
+ class WrappedService(val delegate: ExecutorService) : ExecutorService by delegate {
+ override fun execute(command: Runnable) {
+ delegate.execute {
+ busyThreadsCount.incrementAndGet()
+ try {
+ command.run()
+ } finally {
+ busyThreadsCount.decrementAndGet()
+ }
+ }
+ }
+ }
+ val wrappedExecutor = WrappedService(Executors.newCachedThreadPool())
+ val localDatabase = Room.inMemoryDatabaseBuilder(
+ ApplicationProvider.getApplicationContext(), TestDatabase::class.java)
+ .setQueryExecutor(ArchTaskExecutor.getIOThreadExecutor())
+ .setTransactionExecutor(wrappedExecutor)
+ .build()
+
+ // Run two parallel transactions but verify that only 1 thread is busy when the transactions
+ // execute, indicating that threads are not busy waiting on sql connections but are instead
+ // suspended.
+ runBlocking(Dispatchers.IO) {
+ async {
+ localDatabase.withTransaction {
+ delay(200) // delay a bit to let the other transaction proceed
+ assertThat(busyThreadsCount.get()).isEqualTo(1)
+ }
+ }
+
+ async {
+ localDatabase.withTransaction {
+ delay(200) // delay a bit to let the other transaction proceed
+ assertThat(busyThreadsCount.get()).isEqualTo(1)
+ }
+ }
+ }
+
+ wrappedExecutor.awaitTermination(1, TimeUnit.SECONDS)
+ }
+
+ @Test
+ @Suppress("DeferredResultUnused")
fun withTransaction_leakTransactionContext_async() {
runBlocking {
val leakedContext = database.withTransaction {
@@ -700,7 +749,7 @@
val executorService = Executors.newSingleThreadExecutor()
val localDatabase = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(), TestDatabase::class.java)
- .setQueryExecutor(executorService)
+ .setTransactionExecutor(executorService)
.build()
// Simulate a busy executor, no thread to acquire for transaction.
@@ -735,7 +784,7 @@
val executorService = Executors.newCachedThreadPool()
val localDatabase = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(), TestDatabase::class.java)
- .setQueryExecutor(executorService)
+ .setTransactionExecutor(executorService)
.build()
executorService.shutdownNow()
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTrackerTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTrackerTest.java
index d49779a..6b88e33 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTrackerTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/InvalidationTrackerTest.java
@@ -125,7 +125,7 @@
public void createLiveData() throws ExecutionException, InterruptedException, TimeoutException {
final LiveData<Item> liveData = mDb
.getInvalidationTracker()
- .createLiveData(new String[]{"Item"}, () -> mDb.getItemDao().itemById(1));
+ .createLiveData(new String[]{"Item"}, false, () -> mDb.getItemDao().itemById(1));
mDb.getItemDao().insert(new Item(1, "v1"));
@@ -147,7 +147,7 @@
throws ExecutionException, InterruptedException, TimeoutException {
LiveData<Item> liveData = mDb
.getInvalidationTracker()
- .createLiveData(new String[]{"Item"}, () -> mDb.getItemDao().itemById(1));
+ .createLiveData(new String[]{"Item"}, false, () -> mDb.getItemDao().itemById(1));
mDb.getItemDao().insert(new Item(1, "v1"));
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SneakyThrowTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SneakyThrowTest.java
new file mode 100644
index 0000000..ca96fdc
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/SneakyThrowTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.room.integration.testapp.test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class SneakyThrowTest extends TestDatabaseTest {
+
+ @Test
+ public void testRuntimeException_catchRuntimeException() {
+ try {
+ mDatabase.runInTransaction(() -> {
+ throw new IllegalStateException("Boom");
+ });
+ fail("runInTransaction should have thrown an exception");
+ } catch (IllegalStateException e) {
+ // no-op on purpose
+ }
+ }
+
+ @Test
+ public void testCheckedException_catchThrowable() {
+ try {
+ mDatabase.runInTransaction(() -> {
+ JSONObject json = new JSONObject();
+ return json.getString("key"); // method declares that it throws JSONException
+ });
+ fail("runInTransaction should have thrown an exception");
+ } catch (Throwable e) {
+ assertTrue(e instanceof JSONException);
+ }
+ }
+
+ @Test
+ public void testCheckedException_catchException() {
+ try {
+ mDatabase.runInTransaction(() -> {
+ JSONObject json = new JSONObject();
+ return json.getString("key"); // method declares that it throws JSONException
+ });
+ fail("runInTransaction should have thrown an exception");
+ } catch (Exception e) {
+ assertTrue(e instanceof JSONException);
+ }
+ }
+
+ @Test
+ public void testCheckedException_catchCheckedException() {
+ try {
+ // Must move the lambda to a method that declares throwing the checked exception,
+ // otherwise compiler complains about 'exception not thrown in corresponding block', a
+ // limitation of the sneaky throw technique.
+ doJsonWork();
+ fail("doJsonWork should have thrown an exception");
+ } catch (JSONException e) {
+ // no-op on purpose
+ }
+ }
+
+ private void doJsonWork() throws JSONException {
+ mDatabase.runInTransaction(() -> {
+ JSONObject json = new JSONObject();
+ return json.getString("key"); // method declares that it throws JSONException
+ });
+ }
+}
diff --git a/room/ktx/api/2.1.0-alpha06.txt b/room/ktx/api/2.1.0-alpha06.txt
index f5dcd16..851eb86 100644
--- a/room/ktx/api/2.1.0-alpha06.txt
+++ b/room/ktx/api/2.1.0-alpha06.txt
@@ -1,6 +1,10 @@
// Signature format: 3.0
package androidx.room {
+ public final class CoroutinesRoomKt {
+ ctor public CoroutinesRoomKt();
+ }
+
public final class RoomDatabaseKt {
ctor public RoomDatabaseKt();
method public static suspend Object? acquireTransactionThread(java.util.concurrent.Executor, kotlinx.coroutines.Job controlJob, kotlin.coroutines.experimental.Continuation<? super kotlin.coroutines.ContinuationInterceptor> p);
diff --git a/room/ktx/api/current.txt b/room/ktx/api/current.txt
index f5dcd16..851eb86 100644
--- a/room/ktx/api/current.txt
+++ b/room/ktx/api/current.txt
@@ -1,6 +1,10 @@
// Signature format: 3.0
package androidx.room {
+ public final class CoroutinesRoomKt {
+ ctor public CoroutinesRoomKt();
+ }
+
public final class RoomDatabaseKt {
ctor public RoomDatabaseKt();
method public static suspend Object? acquireTransactionThread(java.util.concurrent.Executor, kotlinx.coroutines.Job controlJob, kotlin.coroutines.experimental.Continuation<? super kotlin.coroutines.ContinuationInterceptor> p);
diff --git a/room/ktx/api/restricted_2.1.0-alpha06.txt b/room/ktx/api/restricted_2.1.0-alpha06.txt
index f31bf4d..866bb4d 100644
--- a/room/ktx/api/restricted_2.1.0-alpha06.txt
+++ b/room/ktx/api/restricted_2.1.0-alpha06.txt
@@ -2,12 +2,12 @@
package androidx.room {
@RestrictTo({RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class CoroutinesRoom {
- method public static suspend <R> Object? execute(androidx.room.RoomDatabase p, java.util.concurrent.Callable<R> db, kotlin.coroutines.experimental.Continuation<? super R> callable);
+ method public static suspend <R> Object? execute(androidx.room.RoomDatabase p, boolean db, java.util.concurrent.Callable<R> inTransaction, kotlin.coroutines.experimental.Continuation<? super R> callable);
field public static final androidx.room.CoroutinesRoom.Companion! Companion;
}
public static final class CoroutinesRoom.Companion {
- method public suspend <R> Object? execute(androidx.room.RoomDatabase db, java.util.concurrent.Callable<R> callable, kotlin.coroutines.experimental.Continuation<? super R> p);
+ method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.experimental.Continuation<? super R> p);
}
}
diff --git a/room/ktx/api/restricted_current.txt b/room/ktx/api/restricted_current.txt
index f31bf4d..866bb4d 100644
--- a/room/ktx/api/restricted_current.txt
+++ b/room/ktx/api/restricted_current.txt
@@ -2,12 +2,12 @@
package androidx.room {
@RestrictTo({RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class CoroutinesRoom {
- method public static suspend <R> Object? execute(androidx.room.RoomDatabase p, java.util.concurrent.Callable<R> db, kotlin.coroutines.experimental.Continuation<? super R> callable);
+ method public static suspend <R> Object? execute(androidx.room.RoomDatabase p, boolean db, java.util.concurrent.Callable<R> inTransaction, kotlin.coroutines.experimental.Continuation<? super R> callable);
field public static final androidx.room.CoroutinesRoom.Companion! Companion;
}
public static final class CoroutinesRoom.Companion {
- method public suspend <R> Object? execute(androidx.room.RoomDatabase db, java.util.concurrent.Callable<R> callable, kotlin.coroutines.experimental.Continuation<? super R> p);
+ method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.experimental.Continuation<? super R> p);
}
}
diff --git a/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index f4e3cc9..d38ed58 100644
--- a/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -17,6 +17,7 @@
package androidx.room
import androidx.annotation.RestrictTo
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import java.util.concurrent.Callable
@@ -33,18 +34,42 @@
companion object {
@JvmStatic
- suspend fun <R> execute(db: RoomDatabase, callable: Callable<R>): R {
+ suspend fun <R> execute(
+ db: RoomDatabase,
+ inTransaction: Boolean,
+ callable: Callable<R>
+ ): R {
if (db.isOpen && db.inTransaction()) {
return callable.call()
}
// Use the transaction dispatcher if we are on a transaction coroutine, otherwise
- // use the query executor as dispatcher.
+ // use the database dispatchers.
val context = coroutineContext[TransactionElement]?.transactionDispatcher
- ?: db.queryExecutor.asCoroutineDispatcher()
+ ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
return withContext(context) {
callable.call()
}
}
}
-}
\ No newline at end of file
+}
+
+/**
+ * Gets the query coroutine dispatcher.
+ *
+ * @hide
+ */
+internal val RoomDatabase.queryDispatcher: CoroutineDispatcher
+ get() = backingFieldMap.getOrPut("QueryDispatcher") {
+ queryExecutor.asCoroutineDispatcher()
+ } as CoroutineDispatcher
+
+/**
+ * Gets the transaction coroutine dispatcher.
+ *
+ * @hide
+ */
+internal val RoomDatabase.transactionDispatcher: CoroutineDispatcher
+ get() = backingFieldMap.getOrPut("TransactionDispatcher") {
+ queryExecutor.asCoroutineDispatcher()
+ } as CoroutineDispatcher
diff --git a/room/ktx/src/main/java/androidx/room/RoomDatabase.kt b/room/ktx/src/main/java/androidx/room/RoomDatabase.kt
index 86f1fde..49a972a 100644
--- a/room/ktx/src/main/java/androidx/room/RoomDatabase.kt
+++ b/room/ktx/src/main/java/androidx/room/RoomDatabase.kt
@@ -91,7 +91,7 @@
*/
private suspend fun RoomDatabase.createTransactionContext(): CoroutineContext {
val controlJob = Job()
- val dispatcher = queryExecutor.acquireTransactionThread(controlJob)
+ val dispatcher = transactionExecutor.acquireTransactionThread(controlJob)
val transactionElement = TransactionElement(controlJob, dispatcher)
val threadLocalElement =
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
diff --git a/room/runtime/api/2.1.0-alpha06.txt b/room/runtime/api/2.1.0-alpha06.txt
index 00d448a..882b112 100644
--- a/room/runtime/api/2.1.0-alpha06.txt
+++ b/room/runtime/api/2.1.0-alpha06.txt
@@ -15,6 +15,7 @@
field public final java.util.concurrent.Executor queryExecutor;
field public final boolean requireMigration;
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ field public final java.util.concurrent.Executor transactionExecutor;
}
public class InvalidationTracker {
@@ -48,6 +49,7 @@
method public androidx.room.InvalidationTracker getInvalidationTracker();
method public androidx.sqlite.db.SupportSQLiteOpenHelper getOpenHelper();
method public java.util.concurrent.Executor getQueryExecutor();
+ method public java.util.concurrent.Executor getTransactionExecutor();
method public boolean inTransaction();
method @CallSuper public void init(androidx.room.DatabaseConfiguration);
method protected void internalInitInvalidationTracker(androidx.sqlite.db.SupportSQLiteDatabase);
@@ -73,6 +75,7 @@
method public androidx.room.RoomDatabase.Builder<T> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
method public androidx.room.RoomDatabase.Builder<T> setJournalMode(androidx.room.RoomDatabase.JournalMode);
method public androidx.room.RoomDatabase.Builder<T> setQueryExecutor(java.util.concurrent.Executor);
+ method public androidx.room.RoomDatabase.Builder<T> setTransactionExecutor(java.util.concurrent.Executor);
}
public abstract static class RoomDatabase.Callback {
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
index 00d448a..882b112 100644
--- a/room/runtime/api/current.txt
+++ b/room/runtime/api/current.txt
@@ -15,6 +15,7 @@
field public final java.util.concurrent.Executor queryExecutor;
field public final boolean requireMigration;
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ field public final java.util.concurrent.Executor transactionExecutor;
}
public class InvalidationTracker {
@@ -48,6 +49,7 @@
method public androidx.room.InvalidationTracker getInvalidationTracker();
method public androidx.sqlite.db.SupportSQLiteOpenHelper getOpenHelper();
method public java.util.concurrent.Executor getQueryExecutor();
+ method public java.util.concurrent.Executor getTransactionExecutor();
method public boolean inTransaction();
method @CallSuper public void init(androidx.room.DatabaseConfiguration);
method protected void internalInitInvalidationTracker(androidx.sqlite.db.SupportSQLiteDatabase);
@@ -73,6 +75,7 @@
method public androidx.room.RoomDatabase.Builder<T> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
method public androidx.room.RoomDatabase.Builder<T> setJournalMode(androidx.room.RoomDatabase.JournalMode);
method public androidx.room.RoomDatabase.Builder<T> setQueryExecutor(java.util.concurrent.Executor);
+ method public androidx.room.RoomDatabase.Builder<T> setTransactionExecutor(java.util.concurrent.Executor);
}
public abstract static class RoomDatabase.Callback {
diff --git a/room/runtime/api/restricted_2.1.0-alpha06.txt b/room/runtime/api/restricted_2.1.0-alpha06.txt
index 10b8084..a792dc1 100644
--- a/room/runtime/api/restricted_2.1.0-alpha06.txt
+++ b/room/runtime/api/restricted_2.1.0-alpha06.txt
@@ -2,7 +2,7 @@
package androidx.room {
public class DatabaseConfiguration {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context, String?, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory, androidx.room.RoomDatabase.MigrationContainer, java.util.List<androidx.room.RoomDatabase.Callback>?, boolean, androidx.room.RoomDatabase.JournalMode!, java.util.concurrent.Executor, boolean, boolean, boolean, java.util.Set<java.lang.Integer>?);
+ ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context, String?, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory, androidx.room.RoomDatabase.MigrationContainer, java.util.List<androidx.room.RoomDatabase.Callback>?, boolean, androidx.room.RoomDatabase.JournalMode!, java.util.concurrent.Executor, java.util.concurrent.Executor, boolean, boolean, boolean, java.util.Set<java.lang.Integer>?);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityDeletionOrUpdateAdapter<T> extends androidx.room.SharedSQLiteStatement {
@@ -32,7 +32,8 @@
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.lang.String...!);
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.util.Map<java.lang.String,java.lang.String>!, java.util.Map<java.lang.String,java.util.Set<java.lang.String>>!, java.lang.String...!);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void addWeakObserver(androidx.room.InvalidationTracker.Observer!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, boolean, java.util.concurrent.Callable<T>!);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @WorkerThread public void refreshVersionsSync();
}
diff --git a/room/runtime/api/restricted_current.txt b/room/runtime/api/restricted_current.txt
index 10b8084..a792dc1 100644
--- a/room/runtime/api/restricted_current.txt
+++ b/room/runtime/api/restricted_current.txt
@@ -2,7 +2,7 @@
package androidx.room {
public class DatabaseConfiguration {
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context, String?, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory, androidx.room.RoomDatabase.MigrationContainer, java.util.List<androidx.room.RoomDatabase.Callback>?, boolean, androidx.room.RoomDatabase.JournalMode!, java.util.concurrent.Executor, boolean, boolean, boolean, java.util.Set<java.lang.Integer>?);
+ ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context, String?, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory, androidx.room.RoomDatabase.MigrationContainer, java.util.List<androidx.room.RoomDatabase.Callback>?, boolean, androidx.room.RoomDatabase.JournalMode!, java.util.concurrent.Executor, java.util.concurrent.Executor, boolean, boolean, boolean, java.util.Set<java.lang.Integer>?);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract class EntityDeletionOrUpdateAdapter<T> extends androidx.room.SharedSQLiteStatement {
@@ -32,7 +32,8 @@
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.lang.String...!);
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(androidx.room.RoomDatabase!, java.util.Map<java.lang.String,java.lang.String>!, java.util.Map<java.lang.String,java.util.Set<java.lang.String>>!, java.lang.String...!);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void addWeakObserver(androidx.room.InvalidationTracker.Observer!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T>! createLiveData(String[]!, boolean, java.util.concurrent.Callable<T>!);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @WorkerThread public void refreshVersionsSync();
}
diff --git a/room/runtime/build.gradle b/room/runtime/build.gradle
index c7e7958..634aea6 100644
--- a/room/runtime/build.gradle
+++ b/room/runtime/build.gradle
@@ -32,6 +32,10 @@
dependencies {
api(project(":room:room-common"))
+ implementation fileTree(
+ dir: "${new File(project(":room:room-common-java8").buildDir, "libs")}",
+ include : "*.jar"
+ )
api(ANDROIDX_SQLITE_FRAMEWORK)
api(ANDROIDX_SQLITE)
implementation(ARCH_CORE_RUNTIME)
@@ -46,6 +50,7 @@
testImplementation(MOCKITO_CORE)
testImplementation(ARCH_LIFECYCLE_EXTENSIONS)
testImplementation(KOTLIN_STDLIB)
+ testImplementation(TRUTH)
androidTestImplementation(JUNIT)
androidTestImplementation(TEST_EXT_JUNIT)
@@ -56,15 +61,21 @@
androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
}
-// Used by testCompile in room-compiler
android.libraryVariants.all { variant ->
def name = variant.name
def suffix = name.capitalize()
+
+ // Create jar<variant> task for testCompile in room-compiler.
project.tasks.create(name: "jar${suffix}", type: Jar){
dependsOn variant.javaCompileProvider.get()
from variant.javaCompileProvider.get().destinationDir
destinationDir new File(project.buildDir, "libJar")
}
+
+ // Make javaCompile task depend on room-common-java8 jar task.
+ variant.javaCompileProvider.configure { task ->
+ task.dependsOn(":room:room-common-java8:jar")
+ }
}
supportLibrary {
diff --git a/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java b/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java
index bb893fd..dae3a09 100644
--- a/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java
+++ b/room/runtime/src/main/java/androidx/room/DatabaseConfiguration.java
@@ -74,6 +74,12 @@
public final Executor queryExecutor;
/**
+ * The Executor used to execute asynchronous transactions.
+ */
+ @NonNull
+ public final Executor transactionExecutor;
+
+ /**
* If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and
* synchronized with other instances of the same {@link RoomDatabase} file, including those
* in a separate process.
@@ -124,6 +130,7 @@
boolean allowMainThreadQueries,
RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
+ @NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@@ -136,6 +143,7 @@
this.allowMainThreadQueries = allowMainThreadQueries;
this.journalMode = journalMode;
this.queryExecutor = queryExecutor;
+ this.transactionExecutor = transactionExecutor;
this.multiInstanceInvalidation = multiInstanceInvalidation;
this.requireMigration = requireMigration;
this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade;
diff --git a/room/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java b/room/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java
index 4168f47..e1e8155 100644
--- a/room/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java
+++ b/room/runtime/src/main/java/androidx/room/InvalidationLiveDataContainer.java
@@ -43,8 +43,10 @@
mDatabase = database;
}
- <T> LiveData<T> create(String[] tableNames, Callable<T> computeFunction) {
- return new RoomTrackingLiveData<>(mDatabase, this, computeFunction, tableNames);
+ <T> LiveData<T> create(String[] tableNames, boolean inTransaction,
+ Callable<T> computeFunction) {
+ return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
+ tableNames);
}
void onActive(LiveData liveData) {
diff --git a/room/runtime/src/main/java/androidx/room/InvalidationTracker.java b/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
index 4d3161a..9e7be31 100644
--- a/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
+++ b/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
@@ -559,6 +559,8 @@
* <p>
* Holds a strong reference to the created LiveData as long as it is active.
*
+ * @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
+ *
* @param computeFunction The function that calculates the value
* @param tableNames The list of tables to observe
* @param <T> The return type
@@ -566,10 +568,32 @@
* invalidates.
* @hide
*/
+ @Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public <T> LiveData<T> createLiveData(String[] tableNames, Callable<T> computeFunction) {
+ return createLiveData(tableNames, false, computeFunction);
+ }
+
+ /**
+ * Creates a LiveData that computes the given function once and for every other invalidation
+ * of the database.
+ * <p>
+ * Holds a strong reference to the created LiveData as long as it is active.
+ *
+ * @param tableNames The list of tables to observe
+ * @param inTransaction True if the computeFunction will be done in a transaction, false
+ * otherwise.
+ * @param computeFunction The function that calculates the value
+ * @param <T> The return type
+ * @return A new LiveData that computes the given function when the given list of tables
+ * invalidates.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
+ Callable<T> computeFunction) {
return mInvalidationLiveDataContainer.create(
- validateAndResolveTableNames(tableNames), computeFunction);
+ validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
}
/**
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
index a33383a..d89ac58 100644
--- a/room/runtime/src/main/java/androidx/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -34,6 +34,7 @@
import androidx.collection.SparseArrayCompat;
import androidx.core.app.ActivityManagerCompat;
import androidx.room.migration.Migration;
+import androidx.room.util.SneakyThrow;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
@@ -45,8 +46,10 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -77,6 +80,7 @@
@Deprecated
protected volatile SupportSQLiteDatabase mDatabase;
private Executor mQueryExecutor;
+ private Executor mTransactionExecutor;
private SupportSQLiteOpenHelper mOpenHelper;
private final InvalidationTracker mInvalidationTracker;
private boolean mAllowMainThreadQueries;
@@ -121,6 +125,19 @@
return mSuspendingTransactionId;
}
+
+ private final Map<String, Object> mBackingFieldMap = new ConcurrentHashMap<>();
+
+ /**
+ * Gets the map for storing extension properties of Kotlin type.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ Map<String, Object> getBackingFieldMap() {
+ return mBackingFieldMap;
+ }
+
/**
* Creates a RoomDatabase.
* <p>
@@ -147,6 +164,7 @@
}
mCallbacks = configuration.callbacks;
mQueryExecutor = configuration.queryExecutor;
+ mTransactionExecutor = new TransactionExecutor(configuration.transactionExecutor);
mAllowMainThreadQueries = configuration.allowMainThreadQueries;
mWriteAheadLoggingEnabled = wal;
if (configuration.multiInstanceInvalidation) {
@@ -336,6 +354,14 @@
}
/**
+ * @return The Executor in use by this database for async transactions.
+ */
+ @NonNull
+ public Executor getTransactionExecutor() {
+ return mTransactionExecutor;
+ }
+
+ /**
* Wrapper for {@link SupportSQLiteDatabase#setTransactionSuccessful()}.
*
* @deprecated Use {@link #runInTransaction(Runnable)}
@@ -380,7 +406,8 @@
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
- throw new RuntimeException("Exception in transaction", e);
+ SneakyThrow.reThrow(e);
+ return null; // Unreachable code, but compiler doesn't know it.
} finally {
endTransaction();
}
@@ -481,6 +508,8 @@
/** The Executor used to run database queries. This should be background-threaded. */
private Executor mQueryExecutor;
+ /** The Executor used to run database transactions. This should be background-threaded. */
+ private Executor mTransactionExecutor;
private SupportSQLiteOpenHelper.Factory mFactory;
private boolean mAllowMainThreadQueries;
private JournalMode mJournalMode;
@@ -598,12 +627,19 @@
* queries and tasks, including {@code LiveData} invalidation, {@code Flowable} scheduling
* and {@code ListenableFuture} tasks.
* <p>
- * When unset, a default {@code Executor} will be used. The default {@code Executor}
- * allocates and shares threads amongst Architecture Components libraries.
+ * When both the query executor and transaction executor are unset, then a default
+ * {@code Executor} will be used. The default {@code Executor} allocates and shares threads
+ * amongst Architecture Components libraries. If the query executor is unset but a
+ * transaction executor was set, then the same {@code Executor} will be used for queries.
+ * <p>
+ * For best performance the given {@code Executor} should be bounded (max number of threads
+ * is limited).
* <p>
* The input {@code Executor} cannot run tasks on the UI thread.
- *
+ **
* @return this
+ *
+ * @see #setTransactionExecutor(Executor)
*/
@NonNull
public Builder<T> setQueryExecutor(@NonNull Executor executor) {
@@ -612,6 +648,32 @@
}
/**
+ * Sets the {@link Executor} that will be used to execute all non-blocking asynchronous
+ * transaction queries and tasks, including {@code LiveData} invalidation, {@code Flowable}
+ * scheduling and {@code ListenableFuture} tasks.
+ * <p>
+ * When both the transaction executor and query executor are unset, then a default
+ * {@code Executor} will be used. The default {@code Executor} allocates and shares threads
+ * amongst Architecture Components libraries. If the transaction executor is unset but a
+ * query executor was set, then the same {@code Executor} will be used for transactions.
+ * <p>
+ * If the given {@code Executor} is shared then it should be unbounded to avoid the
+ * possibility of a deadlock. Room will not use more than one thread at a time from this
+ * executor.
+ * <p>
+ * The input {@code Executor} cannot run tasks on the UI thread.
+ *
+ * @return this
+ *
+ * @see #setQueryExecutor(Executor)
+ */
+ @NonNull
+ public Builder<T> setTransactionExecutor(@NonNull Executor executor) {
+ mTransactionExecutor = executor;
+ return this;
+ }
+
+ /**
* Sets whether table invalidation in this instance of {@link RoomDatabase} should be
* broadcast and synchronized with other instances of the same {@link RoomDatabase},
* including those in a separate process. In order to enable multi-instance invalidation,
@@ -741,8 +803,12 @@
throw new IllegalArgumentException("Must provide an abstract class that"
+ " extends RoomDatabase");
}
- if (mQueryExecutor == null) {
- mQueryExecutor = ArchTaskExecutor.getIOThreadExecutor();
+ if (mQueryExecutor == null && mTransactionExecutor == null) {
+ mQueryExecutor = mTransactionExecutor = ArchTaskExecutor.getIOThreadExecutor();
+ } else if (mQueryExecutor != null && mTransactionExecutor == null) {
+ mTransactionExecutor = mQueryExecutor;
+ } else if (mQueryExecutor == null && mTransactionExecutor != null) {
+ mQueryExecutor = mTransactionExecutor;
}
if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
@@ -763,12 +829,20 @@
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
DatabaseConfiguration configuration =
- new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
- mCallbacks, mAllowMainThreadQueries, mJournalMode.resolve(mContext),
+ new DatabaseConfiguration(
+ mContext,
+ mName,
+ mFactory,
+ mMigrationContainer,
+ mCallbacks,
+ mAllowMainThreadQueries,
+ mJournalMode.resolve(mContext),
mQueryExecutor,
+ mTransactionExecutor,
mMultiInstanceInvalidation,
mRequireMigration,
- mAllowDestructiveMigrationOnDowngrade, mMigrationsNotRequiredFrom);
+ mAllowDestructiveMigrationOnDowngrade,
+ mMigrationsNotRequiredFrom);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
diff --git a/room/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java b/room/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java
index 61ef38e..8df1014 100644
--- a/room/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java
+++ b/room/runtime/src/main/java/androidx/room/RoomTrackingLiveData.java
@@ -27,6 +27,7 @@
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -48,6 +49,9 @@
final RoomDatabase mDatabase;
@SuppressWarnings("WeakerAccess")
+ final boolean mInTransaction;
+
+ @SuppressWarnings("WeakerAccess")
final Callable<T> mComputeFunction;
private final InvalidationLiveDataContainer mContainer;
@@ -116,7 +120,7 @@
boolean isActive = hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
- mDatabase.getQueryExecutor().execute(mRefreshRunnable);
+ getQueryExecutor().execute(mRefreshRunnable);
}
}
}
@@ -125,9 +129,11 @@
RoomTrackingLiveData(
RoomDatabase database,
InvalidationLiveDataContainer container,
+ boolean inTransaction,
Callable<T> computeFunction,
String[] tableNames) {
mDatabase = database;
+ mInTransaction = inTransaction;
mComputeFunction = computeFunction;
mContainer = container;
mObserver = new InvalidationTracker.Observer(tableNames) {
@@ -142,7 +148,7 @@
protected void onActive() {
super.onActive();
mContainer.onActive(this);
- mDatabase.getQueryExecutor().execute(mRefreshRunnable);
+ getQueryExecutor().execute(mRefreshRunnable);
}
@Override
@@ -150,4 +156,12 @@
super.onInactive();
mContainer.onInactive(this);
}
+
+ Executor getQueryExecutor() {
+ if (mInTransaction) {
+ return mDatabase.getTransactionExecutor();
+ } else {
+ return mDatabase.getQueryExecutor();
+ }
+ }
}
diff --git a/room/runtime/src/main/java/androidx/room/TransactionExecutor.java b/room/runtime/src/main/java/androidx/room/TransactionExecutor.java
new file mode 100644
index 0000000..6a6bc12
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/TransactionExecutor.java
@@ -0,0 +1,62 @@
+/*
+ * 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.room;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.Executor;
+
+/**
+ * Executor wrapper for performing database transactions serially.
+ * <p>
+ * Since database transactions are exclusive, this executor ensures that transactions are performed
+ * in-order and one at a time, preventing threads from blocking each other when multiple concurrent
+ * transactions are attempted.
+ */
+class TransactionExecutor implements Executor {
+
+ private final Executor mExecutor;
+ private final ArrayDeque<Runnable> mTasks = new ArrayDeque<>();
+ private Runnable mActive;
+
+ TransactionExecutor(@NonNull Executor executor) {
+ mExecutor = executor;
+ }
+
+ public synchronized void execute(final Runnable command) {
+ mTasks.offer(new Runnable() {
+ public void run() {
+ try {
+ command.run();
+ } finally {
+ scheduleNext();
+ }
+ }
+ });
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ synchronized void scheduleNext() {
+ if ((mActive = mTasks.poll()) != null) {
+ mExecutor.execute(mActive);
+ }
+ }
+}
diff --git a/room/runtime/src/test/java/androidx/room/BuilderTest.java b/room/runtime/src/test/java/androidx/room/BuilderTest.java
index 0c29dd7..eabefdb 100644
--- a/room/runtime/src/test/java/androidx/room/BuilderTest.java
+++ b/room/runtime/src/test/java/androidx/room/BuilderTest.java
@@ -39,6 +39,7 @@
import org.junit.runners.JUnit4;
import java.util.List;
+import java.util.concurrent.Executor;
@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
@RunWith(JUnit4.class)
@@ -66,6 +67,41 @@
Room.databaseBuilder(mock(Context.class), RoomDatabase.class, " ").build();
}
+ public void executors_setQueryExecutor() {
+ Executor executor = mock(Executor.class);
+
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .setQueryExecutor(executor)
+ .build();
+
+ assertThat(db.mDatabaseConfiguration.queryExecutor, is(executor));
+ assertThat(db.mDatabaseConfiguration.transactionExecutor, is(executor));
+ }
+
+ public void executors_setTransactionExecutor() {
+ Executor executor = mock(Executor.class);
+
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .setTransactionExecutor(executor)
+ .build();
+
+ assertThat(db.mDatabaseConfiguration.queryExecutor, is(executor));
+ assertThat(db.mDatabaseConfiguration.transactionExecutor, is(executor));
+ }
+
+ public void executors_setBothExecutors() {
+ Executor executor1 = mock(Executor.class);
+ Executor executor2 = mock(Executor.class);
+
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .setQueryExecutor(executor1)
+ .setTransactionExecutor(executor2)
+ .build();
+
+ assertThat(db.mDatabaseConfiguration.queryExecutor, is(executor1));
+ assertThat(db.mDatabaseConfiguration.transactionExecutor, is(executor2));
+ }
+
@Test
public void migration() {
Migration m1 = new EmptyMigration(0, 1);
@@ -387,6 +423,14 @@
}
abstract static class TestDatabase extends RoomDatabase {
+
+ DatabaseConfiguration mDatabaseConfiguration;
+
+ @Override
+ public void init(@NonNull DatabaseConfiguration configuration) {
+ super.init(configuration);
+ mDatabaseConfiguration = configuration;
+ }
}
static class EmptyMigration extends Migration {
diff --git a/room/runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt b/room/runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
index 97c22d1..6034b67 100644
--- a/room/runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
+++ b/room/runtime/src/test/java/androidx/room/InvalidationLiveDataContainerTest.kt
@@ -99,6 +99,7 @@
private fun createLiveData(): LiveData<Any> {
return container.create(
arrayOf("a", "b"),
+ false,
createComputeFunction<Any>()
) as LiveData
}
diff --git a/room/runtime/src/test/java/androidx/room/TransactionExecutorTest.kt b/room/runtime/src/test/java/androidx/room/TransactionExecutorTest.kt
new file mode 100644
index 0000000..edda059
--- /dev/null
+++ b/room/runtime/src/test/java/androidx/room/TransactionExecutorTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.room
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+@RunWith(JUnit4::class)
+class TransactionExecutorTest {
+
+ private val testExecutor = Executors.newCachedThreadPool()
+ private val transactionExecutor = TransactionExecutor(testExecutor)
+
+ @After
+ fun teardown() {
+ testExecutor.shutdownNow()
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun testSerialExecution() {
+
+ val latch = CountDownLatch(3)
+ val runnableA = TimingRunnable(latch)
+ val runnableB = TimingRunnable(latch)
+ val runnableC = TimingRunnable(latch)
+
+ transactionExecutor.execute(runnableA)
+ transactionExecutor.execute(runnableB)
+ transactionExecutor.execute(runnableC)
+
+ // Await for the runnables to finish.
+ latch.await(1, TimeUnit.SECONDS)
+
+ // Assert that everything ran.
+ assertThat(runnableA.run).isTrue()
+ assertThat(runnableB.run).isTrue()
+ assertThat(runnableC.run).isTrue()
+
+ // Assert that runnables were run in order of submission.
+ assertThat(runnableA.start).isLessThan(runnableB.start)
+ assertThat(runnableB.start).isLessThan(runnableC.start)
+
+ // Assert that a runnable finishes before the runnable after it starts.
+ assertThat(runnableA.finish).isLessThan(runnableB.start)
+ assertThat(runnableB.finish).isLessThan(runnableC.start)
+ }
+
+ private class TimingRunnable(val latch: CountDownLatch) : Runnable {
+ var start: Long = 0
+ var finish: Long = 0
+ var run: Boolean = false
+
+ override fun run() {
+ run = true
+ start = System.nanoTime()
+ try {
+ // Sleep for a bit as if we were doing real work.
+ Thread.sleep(100)
+ } catch (e: InterruptedException) {
+ throw RuntimeException(e)
+ }
+ finish = System.nanoTime()
+ latch.countDown()
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/rxjava2/api/restricted_2.1.0-alpha06.txt b/room/rxjava2/api/restricted_2.1.0-alpha06.txt
index 36290b8..6731dfa 100644
--- a/room/rxjava2/api/restricted_2.1.0-alpha06.txt
+++ b/room/rxjava2/api/restricted_2.1.0-alpha06.txt
@@ -2,8 +2,10 @@
package androidx.room {
public class RxRoom {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, boolean, String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, boolean, String[]!, java.util.concurrent.Callable<T>!);
}
}
diff --git a/room/rxjava2/api/restricted_current.txt b/room/rxjava2/api/restricted_current.txt
index 36290b8..6731dfa 100644
--- a/room/rxjava2/api/restricted_current.txt
+++ b/room/rxjava2/api/restricted_current.txt
@@ -2,8 +2,10 @@
package androidx.room {
public class RxRoom {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Flowable<T>! createFlowable(androidx.room.RoomDatabase!, boolean, String[]!, java.util.concurrent.Callable<T>!);
+ method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, String[]!, java.util.concurrent.Callable<T>!);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static <T> io.reactivex.Observable<T>! createObservable(androidx.room.RoomDatabase!, boolean, String[]!, java.util.concurrent.Callable<T>!);
}
}
diff --git a/room/rxjava2/src/main/java/androidx/room/RxRoom.java b/room/rxjava2/src/main/java/androidx/room/RxRoom.java
index 1e78572..3badde1 100644
--- a/room/rxjava2/src/main/java/androidx/room/RxRoom.java
+++ b/room/rxjava2/src/main/java/androidx/room/RxRoom.java
@@ -20,6 +20,7 @@
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
@@ -97,12 +98,27 @@
* Helper method used by generated code to bind a Callable such that it will be run in
* our disk io thread and will automatically block null values since RxJava2 does not like null.
*
+ * @deprecated Use {@link #createFlowable(RoomDatabase, boolean, String[], Callable)}
+ *
+ * @hide
+ */
+ @Deprecated
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static <T> Flowable<T> createFlowable(final RoomDatabase database,
+ final String[] tableNames, final Callable<T> callable) {
+ return createFlowable(database, false, tableNames, callable);
+ }
+
+ /**
+ * Helper method used by generated code to bind a Callable such that it will be run in
+ * our disk io thread and will automatically block null values since RxJava2 does not like null.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static <T> Flowable<T> createFlowable(final RoomDatabase database,
- final String[] tableNames, final Callable<T> callable) {
- Scheduler scheduler = Schedulers.from(database.getQueryExecutor());
+ final boolean inTransaction, final String[] tableNames, final Callable<T> callable) {
+ Scheduler scheduler = Schedulers.from(getExecutor(database, inTransaction));
final Maybe<T> maybe = Maybe.fromCallable(callable);
return createFlowable(database, tableNames)
.subscribeOn(scheduler)
@@ -161,12 +177,27 @@
* Helper method used by generated code to bind a Callable such that it will be run in
* our disk io thread and will automatically block null values since RxJava2 does not like null.
*
+ * @deprecated Use {@link #createObservable(RoomDatabase, boolean, String[], Callable)}
+ *
+ * @hide
+ */
+ @Deprecated
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static <T> Observable<T> createObservable(final RoomDatabase database,
+ final String[] tableNames, final Callable<T> callable) {
+ return createObservable(database, false, tableNames, callable);
+ }
+
+ /**
+ * Helper method used by generated code to bind a Callable such that it will be run in
+ * our disk io thread and will automatically block null values since RxJava2 does not like null.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static <T> Observable<T> createObservable(final RoomDatabase database,
- final String[] tableNames, final Callable<T> callable) {
- Scheduler scheduler = Schedulers.from(database.getQueryExecutor());
+ final boolean inTransaction, final String[] tableNames, final Callable<T> callable) {
+ Scheduler scheduler = Schedulers.from(getExecutor(database, inTransaction));
final Maybe<T> maybe = Maybe.fromCallable(callable);
return createObservable(database, tableNames)
.subscribeOn(scheduler)
@@ -180,6 +211,14 @@
});
}
+ private static Executor getExecutor(RoomDatabase database, boolean inTransaction) {
+ if (inTransaction) {
+ return database.getTransactionExecutor();
+ } else {
+ return database.getQueryExecutor();
+ }
+ }
+
/** @deprecated This type should not be instantiated as it contains only static methods. */
@Deprecated
@SuppressWarnings("PrivateConstructorForUtilityClass")
diff --git a/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java b/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java
index cc96b50..dd5bebc 100644
--- a/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java
+++ b/room/rxjava2/src/test/java/androidx/room/RxRoomTest.java
@@ -167,7 +167,7 @@
final AtomicReference<String> value = new AtomicReference<>(null);
String[] tables = {"a", "b"};
Set<String> tableSet = new HashSet<>(Arrays.asList(tables));
- final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, tables,
+ final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, false, tables,
new Callable<String>() {
@Override
public String call() throws Exception {
@@ -201,7 +201,7 @@
final AtomicReference<String> value = new AtomicReference<>(null);
String[] tables = {"a", "b"};
Set<String> tableSet = new HashSet<>(Arrays.asList(tables));
- final Observable<String> flowable = RxRoom.createObservable(mDatabase, tables,
+ final Observable<String> flowable = RxRoom.createObservable(mDatabase, false, tables,
new Callable<String>() {
@Override
public String call() throws Exception {
@@ -232,7 +232,7 @@
@Test
public void exception_Flowable() throws Exception {
- final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, new String[]{"a"},
+ final Flowable<String> flowable = RxRoom.createFlowable(mDatabase, false, new String[]{"a"},
new Callable<String>() {
@Override
public String call() throws Exception {
@@ -248,7 +248,8 @@
@Test
public void exception_Observable() throws Exception {
- final Observable<String> flowable = RxRoom.createObservable(mDatabase, new String[]{"a"},
+ final Observable<String> flowable = RxRoom.createObservable(mDatabase, false,
+ new String[]{"a"},
new Callable<String>() {
@Override
public String call() throws Exception {
diff --git a/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
index c0e1c4b..06b5a6b 100644
--- a/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
+++ b/room/testing/src/main/java/androidx/room/testing/MigrationTestHelper.java
@@ -158,6 +158,7 @@
true,
RoomDatabase.JournalMode.TRUNCATE,
ArchTaskExecutor.getIOThreadExecutor(),
+ ArchTaskExecutor.getIOThreadExecutor(),
false,
true,
false,
@@ -215,6 +216,7 @@
true,
RoomDatabase.JournalMode.TRUNCATE,
ArchTaskExecutor.getIOThreadExecutor(),
+ ArchTaskExecutor.getIOThreadExecutor(),
false,
true,
false,
diff --git a/samples/Support4Demos/src/main/AndroidManifest.xml b/samples/Support4Demos/src/main/AndroidManifest.xml
index 52d24be..295caf0 100644
--- a/samples/Support4Demos/src/main/AndroidManifest.xml
+++ b/samples/Support4Demos/src/main/AndroidManifest.xml
@@ -136,14 +136,6 @@
</intent-filter>
</activity>
- <activity android:name=".app.FragmentNestingTabsSupport"
- android:label="@string/fragment_nesting_tabs_support">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
<activity android:name=".app.FragmentRetainInstanceSupport"
android:label="@string/fragment_retain_instance_support">
<intent-filter>
@@ -168,22 +160,6 @@
</intent-filter>
</activity>
- <activity android:name=".app.FragmentTabs"
- android:label="@string/fragment_tabs">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
- <activity android:name=".app.FragmentTabsPager"
- android:label="@string/fragment_tabs_pager">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
- </intent-filter>
- </activity>
-
<activity android:name=".app.FragmentPagerSupport"
android:label="@string/fragment_pager_support">
<intent-filter>
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentNestingTabsSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentNestingTabsSupport.java
index 0991eb8..61b37b7 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentNestingTabsSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentNestingTabsSupport.java
@@ -16,33 +16,4 @@
package com.example.android.supportv4.app;
//BEGIN_INCLUDE(complete)
-
-import android.os.Bundle;
-
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentTabHost;
-
-import com.example.android.supportv4.R;
-
-public class FragmentNestingTabsSupport extends FragmentActivity {
- private FragmentTabHost mTabHost;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mTabHost = new FragmentTabHost(this);
- setContentView(mTabHost);
- mTabHost.setup(this, getSupportFragmentManager(), R.id.fragment1);
-
- mTabHost.addTab(mTabHost.newTabSpec("menus").setIndicator("Menus"),
- FragmentMenuFragmentSupport.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
- LoaderCursorSupport.CursorLoaderListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("stack").setIndicator("Stack"),
- FragmentStackFragmentSupport.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("tabs").setIndicator("Tabs"),
- FragmentTabsFragmentSupport.class, null);
- }
-}
//END_INCLUDE(complete)
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java
index c4bbd93..25cfea4 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabs.java
@@ -16,37 +16,4 @@
package com.example.android.supportv4.app;
//BEGIN_INCLUDE(complete)
-
-import android.os.Bundle;
-
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentTabHost;
-
-import com.example.android.supportv4.R;
-
-/**
- * This demonstrates how you can implement switching between the tabs of a
- * TabHost through fragments, using FragmentTabHost.
- */
-public class FragmentTabs extends FragmentActivity {
- private FragmentTabHost mTabHost;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.fragment_tabs);
- mTabHost = (FragmentTabHost)findViewById(android.R.id.tabhost);
- mTabHost.setup(this, getSupportFragmentManager(), R.id.realtabcontent);
-
- mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),
- FragmentStackSupport.CountingFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
- LoaderCursorSupport.CursorLoaderListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),
- LoaderCustomSupport.AppListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),
- LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);
- }
-}
//END_INCLUDE(complete)
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java
index 06a132b..61b37b7 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java
@@ -16,42 +16,4 @@
package com.example.android.supportv4.app;
//BEGIN_INCLUDE(complete)
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentTabHost;
-
-import com.example.android.supportv4.R;
-
-public class FragmentTabsFragmentSupport extends Fragment {
- private FragmentTabHost mTabHost;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mTabHost = new FragmentTabHost(getActivity());
- mTabHost.setup(getActivity(), getChildFragmentManager(), R.id.fragment1);
-
- mTabHost.addTab(mTabHost.newTabSpec("simple").setIndicator("Simple"),
- FragmentStackSupport.CountingFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("contacts").setIndicator("Contacts"),
- LoaderCursorSupport.CursorLoaderListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("custom").setIndicator("Custom"),
- LoaderCustomSupport.AppListFragment.class, null);
- mTabHost.addTab(mTabHost.newTabSpec("throttle").setIndicator("Throttle"),
- LoaderThrottleSupport.ThrottledLoaderListFragment.class, null);
-
- return mTabHost;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mTabHost = null;
- }
-}
//END_INCLUDE(complete)
diff --git a/samples/Support4Demos/src/main/res/layout/fragment_tabs.xml b/samples/Support4Demos/src/main/res/layout/fragment_tabs.xml
deleted file mode 100644
index faea9cc..0000000
--- a/samples/Support4Demos/src/main/res/layout/fragment_tabs.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/* //device/apps/common/assets/res/layout/tab_content.xml
-**
-** Copyright 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.
-*/
--->
-
-<!-- BEGIN_INCLUDE(complete) -->
-<androidx.fragment.app.FragmentTabHost
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@android:id/tabhost"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TabWidget
- android:id="@android:id/tabs"
- android:orientation="horizontal"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="0"/>
-
- <FrameLayout
- android:id="@android:id/tabcontent"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:layout_weight="0"/>
-
- <FrameLayout
- android:id="@+id/realtabcontent"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"/>
-
- </LinearLayout>
-</androidx.fragment.app.FragmentTabHost>
-<!-- END_INCLUDE(complete) -->
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
index 7b9a95a..501b20e 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SwitchListItemActivity.java
@@ -18,6 +18,7 @@
import android.app.Activity;
import android.content.Context;
+import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.widget.CompoundButton;
import android.widget.Toast;
@@ -103,6 +104,24 @@
mItems.add(item);
item = new SwitchListItem(mContext);
+ item.setPrimaryActionIcon(
+ Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon),
+ SwitchListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item.setTitle("Switch with Icon");
+ item.setBody(longText);
+ item.setSwitchOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new SwitchListItem(mContext);
+ item.setTitle("Switch with Drawable");
+ item.setPrimaryActionIcon(
+ mContext.getDrawable(android.R.drawable.sym_def_app_icon),
+ SwitchListItem.PRIMARY_ACTION_ICON_SIZE_SMALL);
+ item.setBody(longText);
+ item.setSwitchOnCheckedChangeListener(mListener);
+ mItems.add(item);
+
+ item = new SwitchListItem(mContext);
item.setTitle("Clicking item toggles switch");
item.setClickable(true);
item.setSwitchOnCheckedChangeListener(mListener);
diff --git a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoPlayerActivity.java b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoPlayerActivity.java
index 11e0280..0d5594dd 100644
--- a/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoPlayerActivity.java
+++ b/samples/SupportMediaDemos/src/main/java/com/example/androidx/media/VideoPlayerActivity.java
@@ -128,7 +128,7 @@
if (intent == null || (videoUri = intent.getData()) == null || !videoUri.isAbsolute()) {
errorString = "Invalid intent";
} else {
- UriMediaItem mediaItem = new UriMediaItem.Builder(this, videoUri).build();
+ UriMediaItem mediaItem = new UriMediaItem.Builder(videoUri).build();
mVideoView.setMediaItem(mediaItem);
mMediaControlView = new MediaControlView(this);
diff --git a/settings.gradle b/settings.gradle
index 7976ae5..4e26b66 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -147,6 +147,7 @@
includeProject(":room:integration-tests:room-testapp-kotlin", "room/integration-tests/kotlintestapp")
includeProject(":room:room-benchmark", "room/benchmark")
includeProject(":room:room-common", "room/common")
+includeProject(":room:room-common-java8", "room/common-java8")
includeProject(":room:room-compiler", "room/compiler")
includeProject(":room:room-guava", "room/guava")
includeProject(":room:room-ktx", "room/ktx")
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index aa64268..76ea8c1 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -144,10 +144,15 @@
Handler mHandler;
@SuppressWarnings("WeakerAccess") /* synthetic access */
long mLastSentRangeUpdate;
+ // TODO: mRangeValue is in 0..(mRangeMaxValue-mRangeMinValue) at initialization, and in
+ // mRangeMinValue..mRangeMaxValue after user interaction. As far as I know, this doesn't
+ // cause any incorrect behavior, but it is confusing and error-prone.
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mRangeValue;
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mRangeMinValue;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ int mRangeMaxValue;
private SliceItem mRangeItem;
private int mImageSize;
@@ -427,12 +432,10 @@
if (mRowAction != null) {
setViewClickable(mRootView, true);
}
+ mRangeItem = range;
if (!skipSliderUpdate) {
- determineRangeValues(range);
- addRange(range);
- } else {
- // Even if we're skipping the update, we should still update the range item
- mRangeItem = range;
+ setRangeBounds();
+ addRange();
}
return;
}
@@ -602,14 +605,7 @@
}
}
- private void determineRangeValues(SliceItem rangeItem) {
- if (rangeItem == null) {
- mRangeMinValue = 0;
- mRangeValue = 0;
- return;
- }
- mRangeItem = rangeItem;
-
+ private void setRangeBounds() {
SliceItem min = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_MIN);
int minValue = 0;
if (min != null) {
@@ -617,18 +613,27 @@
}
mRangeMinValue = minValue;
- SliceItem progress = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_VALUE);
- if (progress != null) {
- mRangeValue = progress.getInt() - minValue;
+ SliceItem max = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_MAX);
+ int maxValue = 100; // TODO: This default shouldn't be hardcoded here.
+ if (max != null) {
+ maxValue = max.getInt();
}
+ mRangeMaxValue = maxValue;
+
+ SliceItem progress = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_VALUE);
+ int progressValue = 0;
+ if (progress != null) {
+ progressValue = progress.getInt() - minValue;
+ }
+ mRangeValue = progressValue;
}
- private void addRange(final SliceItem range) {
+ private void addRange() {
if (mHandler == null) {
mHandler = new Handler();
}
- final boolean isSeekBar = FORMAT_ACTION.equals(range.getFormat());
+ final boolean isSeekBar = FORMAT_ACTION.equals(mRangeItem.getFormat());
final ProgressBar progressBar = isSeekBar
? new SeekBar(getContext())
: new ProgressBar(getContext(), null, android.R.attr.progressBarStyleHorizontal);
@@ -637,10 +642,9 @@
DrawableCompat.setTint(progressDrawable, mTintColor);
progressBar.setProgressDrawable(progressDrawable);
}
- SliceItem max = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
- if (max != null) {
- progressBar.setMax(max.getInt() - mRangeMinValue);
- }
+ // N.B. We don't use progressBar.setMin because it doesn't work properly in backcompat
+ // and/or sliders.
+ progressBar.setMax(mRangeMaxValue - mRangeMinValue);
progressBar.setProgress(mRangeValue);
progressBar.setVisibility(View.VISIBLE);
addView(progressBar);
@@ -664,21 +668,23 @@
}
void sendSliderValue() {
- if (mRangeItem != null) {
- try {
- mLastSentRangeUpdate = System.currentTimeMillis();
- mRangeItem.fireAction(getContext(),
- new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
- .putExtra(EXTRA_RANGE_VALUE, mRangeValue));
- if (mObserver != null) {
- EventInfo info = new EventInfo(getMode(), ACTION_TYPE_SLIDER, ROW_TYPE_SLIDER,
- mRowIndex);
- info.state = mRangeValue;
- mObserver.onSliceAction(info, mRangeItem);
- }
- } catch (CanceledException e) {
- Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+ if (mRangeItem == null) {
+ return;
+ }
+
+ try {
+ mLastSentRangeUpdate = System.currentTimeMillis();
+ mRangeItem.fireAction(getContext(),
+ new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ .putExtra(EXTRA_RANGE_VALUE, mRangeValue));
+ if (mObserver != null) {
+ EventInfo info = new EventInfo(getMode(), ACTION_TYPE_SLIDER, ROW_TYPE_SLIDER,
+ mRowIndex);
+ info.state = mRangeValue;
+ mObserver.onSliceAction(info, mRangeItem);
}
+ } catch (CanceledException e) {
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
}
@@ -904,6 +910,7 @@
mRangeHasPendingUpdate = false;
mRangeItem = null;
mRangeMinValue = 0;
+ mRangeMaxValue = 0;
mRangeValue = 0;
mLastSentRangeUpdate = 0;
mHandler = null;
diff --git a/testutils/src/main/java/androidx/testutils/FragmentActivityUtils.java b/testutils/src/main/java/androidx/testutils/FragmentActivityUtils.java
index 22d0edd..615d1a2 100644
--- a/testutils/src/main/java/androidx/testutils/FragmentActivityUtils.java
+++ b/testutils/src/main/java/androidx/testutils/FragmentActivityUtils.java
@@ -90,8 +90,8 @@
ActivityTestRule<? extends RecreatedActivity> rule, final T activity)
throws InterruptedException {
// Now switch the orientation
- RecreatedActivity.sResumed = new CountDownLatch(1);
- RecreatedActivity.sDestroyed = new CountDownLatch(1);
+ RecreatedActivity.setResumedLatch(new CountDownLatch(1));
+ RecreatedActivity.setDestroyedLatch(new CountDownLatch(1));
runOnUiThreadRethrow(rule, new Runnable() {
@Override
@@ -99,9 +99,9 @@
activity.recreate();
}
});
- assertTrue(RecreatedActivity.sResumed.await(1, TimeUnit.SECONDS));
- assertTrue(RecreatedActivity.sDestroyed.await(1, TimeUnit.SECONDS));
- T newActivity = (T) RecreatedActivity.sActivity;
+ assertTrue(RecreatedActivity.getResumedLatch().await(1, TimeUnit.SECONDS));
+ assertTrue(RecreatedActivity.getDestroyedLatch().await(1, TimeUnit.SECONDS));
+ T newActivity = (T) RecreatedActivity.getActivity();
waitForExecution(rule);
diff --git a/testutils/src/main/java/androidx/testutils/RecreatedActivity.java b/testutils/src/main/java/androidx/testutils/RecreatedActivity.java
deleted file mode 100644
index 34326a48..0000000
--- a/testutils/src/main/java/androidx/testutils/RecreatedActivity.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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 androidx.testutils;
-
-import android.os.Bundle;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentActivity;
-import androidx.test.rule.ActivityTestRule;
-
-import java.util.concurrent.CountDownLatch;
-
-/**
- * Extension of {@link FragmentActivity} that keeps track of when it is recreated.
- * In order to use this class, have your activity extend it and call
- * {@link FragmentActivityUtils#recreateActivity(ActivityTestRule, RecreatedActivity)} API.
- */
-public class RecreatedActivity extends FragmentActivity {
- // These must be cleared after each test using clearState()
- public static RecreatedActivity sActivity;
- public static CountDownLatch sResumed;
- public static CountDownLatch sDestroyed;
-
- static void clearState() {
- sActivity = null;
- sResumed = null;
- sDestroyed = null;
- }
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- sActivity = this;
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (sResumed != null) {
- sResumed.countDown();
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (sDestroyed != null) {
- sDestroyed.countDown();
- }
- }
-}
diff --git a/testutils/src/main/java/androidx/testutils/RecreatedActivity.kt b/testutils/src/main/java/androidx/testutils/RecreatedActivity.kt
new file mode 100644
index 0000000..22b74aa
--- /dev/null
+++ b/testutils/src/main/java/androidx/testutils/RecreatedActivity.kt
@@ -0,0 +1,61 @@
+/*
+ * 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 androidx.testutils
+
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import java.util.concurrent.CountDownLatch
+
+/**
+ * Extension of [FragmentActivity] that keeps track of when it is recreated.
+ * In order to use this class, have your activity extend it and call
+ * [FragmentActivityUtils.recreateActivity] API.
+ */
+open class RecreatedActivity : FragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ activity = this
+ }
+
+ override fun onResume() {
+ super.onResume()
+ resumedLatch?.countDown()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ destroyedLatch?.countDown()
+ }
+
+ companion object {
+ // These must be cleared after each test using clearState()
+ @JvmStatic
+ var activity: RecreatedActivity? = null
+ @JvmStatic
+ var resumedLatch: CountDownLatch? = null
+ @JvmStatic
+ var destroyedLatch: CountDownLatch? = null
+
+ @JvmStatic
+ fun clearState() {
+ activity = null
+ resumedLatch = null
+ destroyedLatch = null
+ }
+ }
+}
diff --git a/textclassifier/api/1.0.0-alpha03.txt b/textclassifier/api/1.0.0-alpha03.txt
index 043c858..c37c7d2 100644
--- a/textclassifier/api/1.0.0-alpha03.txt
+++ b/textclassifier/api/1.0.0-alpha03.txt
@@ -1,6 +1,10 @@
// Signature format: 3.0
package androidx.textclassifier {
+ public final class ExtrasUtils {
+ method public static java.util.Locale? getTopLanguage(android.content.Intent?);
+ }
+
public final class TextClassification {
method public static androidx.textclassifier.TextClassification createFromBundle(android.os.Bundle);
method public java.util.List<androidx.core.app.RemoteActionCompat> getActions();
diff --git a/textclassifier/api/current.txt b/textclassifier/api/current.txt
index 043c858..c37c7d2 100644
--- a/textclassifier/api/current.txt
+++ b/textclassifier/api/current.txt
@@ -1,6 +1,10 @@
// Signature format: 3.0
package androidx.textclassifier {
+ public final class ExtrasUtils {
+ method public static java.util.Locale? getTopLanguage(android.content.Intent?);
+ }
+
public final class TextClassification {
method public static androidx.textclassifier.TextClassification createFromBundle(android.os.Bundle);
method public java.util.List<androidx.core.app.RemoteActionCompat> getActions();
diff --git a/textclassifier/src/androidTest/java/androidx/textclassifier/ExtrasUtilsTest.java b/textclassifier/src/androidTest/java/androidx/textclassifier/ExtrasUtilsTest.java
new file mode 100644
index 0000000..17511e0
--- /dev/null
+++ b/textclassifier/src/androidTest/java/androidx/textclassifier/ExtrasUtilsTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.textclassifier;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Intent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link ExtrasUtils}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ExtrasUtilsTest {
+
+ @Test
+ public void testGetTopLanguage() {
+ final Intent intent = ExtrasUtils.buildFakeTextClassifierIntent("ja", "en");
+ assertThat(ExtrasUtils.getTopLanguage(intent).getLanguage()).isEqualTo("ja");
+ }
+
+ @Test
+ public void testGetTopLanguage_differentLanguage() {
+ final Intent intent = ExtrasUtils.buildFakeTextClassifierIntent("de");
+ assertThat(ExtrasUtils.getTopLanguage(intent).getLanguage()).isEqualTo("de");
+ }
+
+ @Test
+ public void testGetTopLanguage_nullLanguageBundle() {
+ assertThat(ExtrasUtils.getTopLanguage(new Intent())).isNull();
+ }
+
+ @Test
+ public void testGetTopLanguage_null() {
+ assertThat(ExtrasUtils.getTopLanguage(null)).isNull();
+ }
+}
diff --git a/textclassifier/src/main/java/androidx/textclassifier/BundleUtils.java b/textclassifier/src/main/java/androidx/textclassifier/BundleUtils.java
index 1906b4c..52704ec 100644
--- a/textclassifier/src/main/java/androidx/textclassifier/BundleUtils.java
+++ b/textclassifier/src/main/java/androidx/textclassifier/BundleUtils.java
@@ -25,6 +25,7 @@
import androidx.collection.ArrayMap;
import androidx.core.app.RemoteActionCompat;
import androidx.core.os.LocaleListCompat;
+import androidx.versionedparcelable.ParcelUtils;
import java.util.ArrayList;
import java.util.List;
@@ -80,30 +81,13 @@
/** Serializes a list of actions to a bundle, or clears it if null is passed. */
static void putRemoteActionList(
@NonNull Bundle bundle, @NonNull String key,
- @Nullable List<RemoteActionCompat> actions) {
- if (actions == null) {
- bundle.remove(key);
- return;
- }
- final ArrayList<Bundle> actionBundles = new ArrayList<>(actions.size());
- for (RemoteActionCompat action : actions) {
- actionBundles.add(action.toBundle());
- }
- bundle.putParcelableArrayList(key, actionBundles);
+ @NonNull List<RemoteActionCompat> actions) {
+ ParcelUtils.putVersionedParcelableList(bundle, key, actions);
}
- /** @throws IllegalArgumentException if key can't be found in the bundle */
static List<RemoteActionCompat> getRemoteActionListOrThrow(
@NonNull Bundle bundle, @NonNull String key) {
- final List<Bundle> actionBundles = bundle.getParcelableArrayList(key);
- if (actionBundles == null) {
- throw new IllegalArgumentException("Missing " + key);
- }
- final List<RemoteActionCompat> actions = new ArrayList<>(actionBundles.size());
- for (Bundle actionBundle : actionBundles) {
- actions.add(RemoteActionCompat.createFromBundle(actionBundle));
- }
- return actions;
+ return ParcelUtils.getVersionedParcelableList(bundle, key);
}
/** Serializes a list of TextLinks to a bundle, or clears it if null is passed. */
diff --git a/textclassifier/src/main/java/androidx/textclassifier/ExtrasUtils.java b/textclassifier/src/main/java/androidx/textclassifier/ExtrasUtils.java
new file mode 100644
index 0000000..26c45c6
--- /dev/null
+++ b/textclassifier/src/main/java/androidx/textclassifier/ExtrasUtils.java
@@ -0,0 +1,114 @@
+/*
+ * 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.textclassifier;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.os.LocaleListCompat;
+
+import java.util.Locale;
+
+/**
+ * Utilities for inserting/retrieving data into/from textclassifier related results and intents.
+ */
+public final class ExtrasUtils {
+
+ private static final String TAG = "ExtrasUtils";
+
+ private static final String EXTRA_FROM_TEXT_CLASSIFIER =
+ "android.view.textclassifier.extra.FROM_TEXT_CLASSIFIER";
+ private static final String ENTITY_TYPE = "entity-type";
+ private static final String SCORE = "score";
+ private static final String TEXT_LANGUAGES = "text-languages";
+
+ private ExtrasUtils() {}
+
+ /**
+ * Returns the highest scoring language found in the textclassifier extras in the intent.
+ * This may return null if the data could not be found.
+ *
+ * @param intent the intent used to start the activity.
+ * @see android.app.Activity#getIntent()
+ */
+ @Nullable
+ public static Locale getTopLanguage(@Nullable Intent intent) {
+ try {
+ // NOTE: This is (and should be) a copy of the related platform code.
+ // It is hard to test this code returns something on a given platform because we can't
+ // guarantee the TextClassifier implementation that will be used to send the intent.
+ // Depend on the platform tests instead and avoid this code running out of sync with
+ // what is expected of each platform. Note that the code may differ from platform to
+ // platform but that will be a bad idea as it will be hard to manage.
+ // TODO: Include a "put" counterpart of this method so that other TextClassifier
+ // implementations may use it to put language data into the generated intent in a way
+ // that this method can retrieve it.
+ if (intent == null) {
+ return null;
+ }
+ final Bundle tcBundle = intent.getBundleExtra(EXTRA_FROM_TEXT_CLASSIFIER);
+ if (tcBundle == null) {
+ return null;
+ }
+ final Bundle textLanguagesExtra = tcBundle.getBundle(TEXT_LANGUAGES);
+ if (textLanguagesExtra == null) {
+ return null;
+ }
+ final String[] languages = textLanguagesExtra.getStringArray(ENTITY_TYPE);
+ final float[] scores = textLanguagesExtra.getFloatArray(SCORE);
+ if (languages == null || scores == null
+ || languages.length == 0 || languages.length != scores.length) {
+ return null;
+ }
+ int highestScoringIndex = 0;
+ for (int i = 1; i < languages.length; i++) {
+ if (scores[highestScoringIndex] < scores[i]) {
+ highestScoringIndex = i;
+ }
+ }
+ final LocaleListCompat localeList =
+ LocaleListCompat.forLanguageTags(languages[highestScoringIndex]);
+ return localeList.isEmpty() ? null : localeList.get(0);
+ } catch (Throwable t) {
+ // Prevent this method from crashing the process.
+ Log.e(TAG, "Error retrieving language information from textclassifier intent", t);
+ return null;
+ }
+ }
+
+ /**
+ * Returns a fake TextClassifier generated intent for testing purposes.
+ * @param languages ordered list of languages for the classified text
+ */
+ @VisibleForTesting
+ static Intent buildFakeTextClassifierIntent(String... languages) {
+ final float[] scores = new float[languages.length];
+ float scoresLeft = 1f;
+ for (int i = 0; i < scores.length; i++) {
+ scores[i] = scoresLeft /= 2;
+ }
+ final Bundle textLanguagesExtra = new Bundle();
+ textLanguagesExtra.putStringArray(ENTITY_TYPE, languages);
+ textLanguagesExtra.putFloatArray(SCORE, scores);
+ final Bundle tcBundle = new Bundle();
+ tcBundle.putBundle(TEXT_LANGUAGES, textLanguagesExtra);
+ return new Intent(Intent.ACTION_VIEW).putExtra(EXTRA_FROM_TEXT_CLASSIFIER, tcBundle);
+ }
+}
diff --git a/versionedparcelable/api/1.1.0-alpha02.txt b/versionedparcelable/api/1.1.0-alpha02.txt
index a53654a..0ca14c1 100644
--- a/versionedparcelable/api/1.1.0-alpha02.txt
+++ b/versionedparcelable/api/1.1.0-alpha02.txt
@@ -3,7 +3,9 @@
public class ParcelUtils {
method public static <T extends androidx.versionedparcelable.VersionedParcelable> T? getVersionedParcelable(android.os.Bundle!, String!);
+ method public static <T extends androidx.versionedparcelable.VersionedParcelable> java.util.List<T>? getVersionedParcelableList(android.os.Bundle!, String!);
method public static void putVersionedParcelable(android.os.Bundle, String, androidx.versionedparcelable.VersionedParcelable);
+ method public static void putVersionedParcelableList(android.os.Bundle, String, java.util.List<? extends androidx.versionedparcelable.VersionedParcelable>);
}
public interface VersionedParcelable {
diff --git a/versionedparcelable/api/current.txt b/versionedparcelable/api/current.txt
index a53654a..0ca14c1 100644
--- a/versionedparcelable/api/current.txt
+++ b/versionedparcelable/api/current.txt
@@ -3,7 +3,9 @@
public class ParcelUtils {
method public static <T extends androidx.versionedparcelable.VersionedParcelable> T? getVersionedParcelable(android.os.Bundle!, String!);
+ method public static <T extends androidx.versionedparcelable.VersionedParcelable> java.util.List<T>? getVersionedParcelableList(android.os.Bundle!, String!);
method public static void putVersionedParcelable(android.os.Bundle, String, androidx.versionedparcelable.VersionedParcelable);
+ method public static void putVersionedParcelableList(android.os.Bundle, String, java.util.List<? extends androidx.versionedparcelable.VersionedParcelable>);
}
public interface VersionedParcelable {
diff --git a/versionedparcelable/src/main/java/androidx/versionedparcelable/ParcelUtils.java b/versionedparcelable/src/main/java/androidx/versionedparcelable/ParcelUtils.java
index b9b3b04..c2d530b 100644
--- a/versionedparcelable/src/main/java/androidx/versionedparcelable/ParcelUtils.java
+++ b/versionedparcelable/src/main/java/androidx/versionedparcelable/ParcelUtils.java
@@ -27,6 +27,8 @@
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
/**
* Utilities for managing {@link VersionedParcelable}s.
@@ -91,7 +93,6 @@
b.putParcelable(key, innerBundle);
}
-
/**
* Get a VersionedParcelable from a Bundle.
*
@@ -109,4 +110,43 @@
return null;
}
}
+
+ /**
+ * Add a list of VersionedParcelable to an existing Bundle.
+ */
+ public static void putVersionedParcelableList(@NonNull Bundle b, @NonNull String key,
+ @NonNull List<? extends VersionedParcelable> list) {
+ Bundle innerBundle = new Bundle();
+ ArrayList<Parcelable> toWrite = new ArrayList<>();
+ for (VersionedParcelable obj : list) {
+ toWrite.add(toParcelable(obj));
+ }
+ innerBundle.putParcelableArrayList(INNER_BUNDLE_KEY, toWrite);
+ b.putParcelable(key, innerBundle);
+ }
+
+ /**
+ * Get a list of VersionedParcelable from a Bundle.
+ *
+ * Returns null if the bundle isn't present or ClassLoader issues occur.
+ */
+ @SuppressWarnings("TypeParameterUnusedInFormals")
+ @Nullable
+ public static <T extends VersionedParcelable> List<T> getVersionedParcelableList(
+ Bundle bundle, String key) {
+ List<T> resultList = new ArrayList<>();
+ try {
+ Bundle innerBundle = bundle.getParcelable(key);
+ innerBundle.setClassLoader(ParcelUtils.class.getClassLoader());
+ ArrayList<Parcelable> parcelableArrayList =
+ innerBundle.getParcelableArrayList(INNER_BUNDLE_KEY);
+ for (Parcelable parcelable : parcelableArrayList) {
+ resultList.add((T) fromParcelable(parcelable));
+ }
+ return resultList;
+ } catch (RuntimeException e) {
+ // There may be new classes or such in the bundle, make sure not to crash the caller.
+ }
+ return null;
+ }
}
diff --git a/viewpager2/integration-tests/testapp/build.gradle b/viewpager2/integration-tests/testapp/build.gradle
index be2719a..3affaad 100644
--- a/viewpager2/integration-tests/testapp/build.gradle
+++ b/viewpager2/integration-tests/testapp/build.gradle
@@ -33,6 +33,7 @@
implementation(project(":viewpager2"))
implementation(ARCH_LIFECYCLE_EXTENSIONS)
implementation(MATERIAL)
+ implementation(project(":coordinatorlayout"))
implementation(project(":cardview"))
androidTestImplementation(TEST_RULES)
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
index c70085e..54f92c2 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/BaseTest.kt
@@ -39,7 +39,7 @@
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
-import androidx.testutils.FragmentActivityUtils
+import androidx.testutils.AppCompatActivityUtils
import androidx.testutils.FragmentActivityUtils.waitForActivityDrawn
import androidx.viewpager2.LocaleTestUtils
import androidx.viewpager2.adapter.FragmentStateAdapter
@@ -120,7 +120,7 @@
viewPager.adapter = adapterProvider(activity)
onCreateCallback(viewPager)
}
- activity = FragmentActivityUtils.recreateActivity(activityTestRule, activity)
+ activity = AppCompatActivityUtils.recreateActivity(activityTestRule, activity)
TestActivity.onCreateCallback = { }
waitForActivityDrawn(activity)
}
diff --git a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
index f4ed3c6..7715091 100644
--- a/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
+++ b/viewpager2/src/androidTest/java/androidx/viewpager2/widget/swipe/TestActivity.kt
@@ -17,11 +17,11 @@
package androidx.viewpager2.widget.swipe
import android.os.Bundle
-import androidx.testutils.RecreatedActivity
+import androidx.testutils.RecreatedAppCompatActivity
import androidx.viewpager2.LocaleTestUtils
import androidx.viewpager2.test.R
-class TestActivity : RecreatedActivity() {
+class TestActivity : RecreatedAppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent?.hasExtra(EXTRA_LANGUAGE) == true) {
diff --git a/viewpager2/src/androidTest/res/values/styles.xml b/viewpager2/src/androidTest/res/values/styles.xml
index 94e0a86..cb1f73d 100644
--- a/viewpager2/src/androidTest/res/values/styles.xml
+++ b/viewpager2/src/androidTest/res/values/styles.xml
@@ -15,7 +15,7 @@
-->
<resources>
- <style name="TestActivityTheme" parent="android:Theme">
+ <style name="TestActivityTheme" parent="Theme.AppCompat">
<item name="android:windowAnimationStyle">@empty</item>
</style>
</resources>
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
index e1fe81f..2ec6943 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
@@ -17,25 +17,22 @@
package androidx.webkit;
import android.app.Activity;
-import android.net.Uri;
import android.os.Bundle;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
-import android.webkit.WebViewClient;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
import org.junit.Assert;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.Callable;
-
@RunWith(AndroidJUnit4.class)
public class WebViewAssetLoaderIntegrationTest {
private static final String TAG = "WebViewAssetLoaderIntegrationTest";
@@ -44,67 +41,58 @@
public final ActivityTestRule<TestActivity> mActivityRule =
new ActivityTestRule<>(TestActivity.class);
+ private WebViewOnUiThread mOnUiThread;
+ private WebViewAssetLoader mAssetLoader;
+
+ private static class AssetLoadingWebViewClient extends WebViewOnUiThread.WaitForLoadedClient {
+ private final WebViewAssetLoader mAssetLoader;
+ AssetLoadingWebViewClient(WebViewOnUiThread onUiThread,
+ WebViewAssetLoader assetLoader) {
+ super(onUiThread);
+ mAssetLoader = assetLoader;
+ }
+
+ @SuppressWarnings({"deprecated"})
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+ return mAssetLoader.shouldInterceptRequest(url);
+ }
+
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view,
+ WebResourceRequest request) {
+ return mAssetLoader.shouldInterceptRequest(request);
+ }
+ }
+
// An Activity for Integeration tests
public static class TestActivity extends Activity {
- private class MyWebViewClient extends WebViewClient {
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- return false;
- }
-
- @Override
- public void onPageFinished(WebView view, String url) {
- mOnPageFinishedUrl.add(url);
- }
-
- @SuppressWarnings({"deprecated"})
- @Override
- public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
- return mAssetLoader.shouldInterceptRequest(url);
- }
-
- @Override
- public WebResourceResponse shouldInterceptRequest(WebView view,
- WebResourceRequest request) {
- return mAssetLoader.shouldInterceptRequest(request);
- }
- }
-
- private WebViewAssetLoader mAssetLoader;
private WebView mWebView;
- private ArrayBlockingQueue<String> mOnPageFinishedUrl = new ArrayBlockingQueue<String>(5);
-
- public WebViewAssetLoader getAssetLoader() {
- return mAssetLoader;
-
- }
public WebView getWebView() {
return mWebView;
}
- public ArrayBlockingQueue<String> getOnPageFinishedUrl() {
- return mOnPageFinishedUrl;
- }
-
- private void setUpWebView(WebView view) {
- view.setWebViewClient(new MyWebViewClient());
- }
-
+ // Runs before test suite's @Before.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- mAssetLoader = (new WebViewAssetLoader.Builder(this)).build();
mWebView = new WebView(this);
- setUpWebView(mWebView);
setContentView(mWebView);
}
+ }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mWebView.destroy();
- mWebView = null;
+ @Before
+ public void setUp() {
+ mAssetLoader = (new WebViewAssetLoader.Builder(mActivityRule.getActivity())).build();
+ mOnUiThread = new WebViewOnUiThread(mActivityRule.getActivity().getWebView());
+ mOnUiThread.setWebViewClient(new AssetLoadingWebViewClient(mOnUiThread, mAssetLoader));
+ }
+
+ @After
+ public void tearDown() {
+ if (mOnUiThread != null) {
+ mOnUiThread.cleanUp();
}
}
@@ -112,66 +100,34 @@
@MediumTest
public void testAssetHosting() throws Exception {
final TestActivity activity = mActivityRule.getActivity();
- final String test_with_title_path = "www/test_with_title.html";
+ final String testWithTitlePath = "www/test_with_title.html";
- String url = WebkitUtils.onMainThreadSync(new Callable<String>() {
- @Override
- public String call() {
- WebViewAssetLoader assetLoader = activity.getAssetLoader();
- Uri.Builder testPath =
- assetLoader.getAssetsHttpsPrefix().buildUpon()
- .appendPath(test_with_title_path);
+ String url =
+ mAssetLoader.getAssetsHttpsPrefix().buildUpon()
+ .appendPath(testWithTitlePath)
+ .build()
+ .toString();
- String url = testPath.toString();
- activity.getWebView().loadUrl(url);
+ mOnUiThread.loadUrlAndWaitForCompletion(url);
- return url;
- }
- });
-
- String onPageFinishedUrl = activity.getOnPageFinishedUrl().take();
- Assert.assertEquals(url, onPageFinishedUrl);
-
- String title = WebkitUtils.onMainThreadSync(new Callable<String>() {
- @Override
- public String call() {
- return activity.getWebView().getTitle();
- }
- });
- Assert.assertEquals("WebViewAssetLoaderTest", title);
+ Assert.assertEquals("WebViewAssetLoaderTest", mOnUiThread.getTitle());
}
@Test
@MediumTest
public void testResourcesHosting() throws Exception {
final TestActivity activity = mActivityRule.getActivity();
- final String test_with_title_path = "test_with_title.html";
+ final String testWithTitlePath = "test_with_title.html";
- String url = WebkitUtils.onMainThreadSync(new Callable<String>() {
- @Override
- public String call() {
- WebViewAssetLoader assetLoader = activity.getAssetLoader();
- Uri.Builder testPath =
- assetLoader.getResourcesHttpsPrefix().buildUpon()
- .appendPath("raw")
- .appendPath(test_with_title_path);
+ String url =
+ mAssetLoader.getResourcesHttpsPrefix().buildUpon()
+ .appendPath("raw")
+ .appendPath(testWithTitlePath)
+ .build()
+ .toString();
- String url = testPath.toString();
- activity.getWebView().loadUrl(url);
+ mOnUiThread.loadUrlAndWaitForCompletion(url);
- return url;
- }
- });
-
- String onPageFinishedUrl = activity.getOnPageFinishedUrl().take();
- Assert.assertEquals(url, onPageFinishedUrl);
-
- String title = WebkitUtils.onMainThreadSync(new Callable<String>() {
- @Override
- public String call() {
- return activity.getWebView().getTitle();
- }
- });
- Assert.assertEquals("WebViewAssetLoaderTest", title);
+ Assert.assertEquals("WebViewAssetLoaderTest", mOnUiThread.getTitle());
}
}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderTest.java
index ff62e4b..bf663db 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderTest.java
@@ -18,7 +18,6 @@
import android.content.ContextWrapper;
import android.net.Uri;
-import android.util.Log;
import android.webkit.WebResourceResponse;
import androidx.test.core.app.ApplicationProvider;
@@ -95,9 +94,8 @@
try {
return new ByteArrayInputStream(contents.getBytes(encoding));
} catch (UnsupportedEncodingException e) {
- Log.e(TAG, "exception when creating response", e);
+ throw new RuntimeException(e);
}
- return null;
}
};
@@ -106,10 +104,11 @@
WebResourceResponse response =
assetLoader.shouldInterceptRequest("http://appassets.androidplatform.net/test/");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("didn't match the exact registered URL", response);
Assert.assertEquals(contents, readAsString(response.getData(), encoding));
- Assert.assertNull(assetLoader.shouldInterceptRequest("http://foo.bar/"));
+ Assert.assertNull("opened a non-registered URL - should return null",
+ assetLoader.shouldInterceptRequest("http://foo.bar/"));
}
@Test
@@ -125,21 +124,23 @@
try {
return new ByteArrayInputStream(testHtmlContents.getBytes("utf-8"));
} catch (IOException e) {
- Log.e(TAG, "Unable to open asset URL: " + url);
- return null;
+ throw new RuntimeException(e);
}
}
return null;
}
});
- Assert.assertNull(assetLoader.getAssetsHttpPrefix());
+ Assert.assertNull("HTTP is not allowed - getAssetsHttpPrefix should return null",
+ assetLoader.getAssetsHttpPrefix());
Assert.assertEquals(assetLoader.getAssetsHttpsPrefix(),
Uri.parse("https://appassets.androidplatform.net/assets/"));
WebResourceResponse response =
assetLoader.shouldInterceptRequest("https://appassets.androidplatform.net/assets/www/test.html");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("failed to match the URL and returned null response", response);
+ Assert.assertNotNull("matched the URL but not the file and returned a null InputStream",
+ response.getData());
Assert.assertEquals(testHtmlContents, readAsString(response.getData(), "utf-8"));
}
@@ -152,23 +153,27 @@
WebViewAssetLoader assetLoader = builder.buildForTest(new MockAssetHelper() {
@Override
public InputStream openResource(Uri uri) {
- try {
- if (uri.getPath().equals("raw/test.html")) {
+ if (uri.getPath().equals("raw/test.html")) {
+ try {
return new ByteArrayInputStream(testHtmlContents.getBytes("utf-8"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
- } catch (IOException e) {
- Log.e(TAG, "exception when creating response", e);
}
return null;
}
});
- Assert.assertNull(assetLoader.getResourcesHttpPrefix());
- Assert.assertEquals(assetLoader.getResourcesHttpsPrefix(), Uri.parse("https://appassets.androidplatform.net/res/"));
+ Assert.assertNull("HTTP is not allowed - getResourcesHttpPrefix should return null",
+ assetLoader.getResourcesHttpPrefix());
+ Assert.assertEquals(assetLoader.getResourcesHttpsPrefix(),
+ Uri.parse("https://appassets.androidplatform.net/res/"));
WebResourceResponse response =
assetLoader.shouldInterceptRequest("https://appassets.androidplatform.net/res/raw/test.html");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("failed to match the URL and returned null response", response);
+ Assert.assertNotNull("matched the prefix URL but not the file",
+ response.getData());
Assert.assertEquals(testHtmlContents, readAsString(response.getData(), "utf-8"));
}
@@ -188,8 +193,7 @@
try {
return new ByteArrayInputStream(testHtmlContents.getBytes("utf-8"));
} catch (IOException e) {
- Log.e(TAG, "Unable to open asset URL: " + url);
- return null;
+ throw new RuntimeException(e);
}
}
return null;
@@ -203,7 +207,9 @@
WebResourceResponse response =
assetLoader.shouldInterceptRequest("http://example.com/android_assets/www/test.html");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("failed to match the URL and returned null response", response);
+ Assert.assertNotNull("matched the prefix URL but not the file",
+ response.getData());
Assert.assertEquals(testHtmlContents, readAsString(response.getData(), "utf-8"));
}
@@ -219,12 +225,12 @@
WebViewAssetLoader assetLoader = builder.buildForTest(new MockAssetHelper() {
@Override
public InputStream openResource(Uri uri) {
- try {
- if (uri.getPath().equals("raw/test.html")) {
+ if (uri.getPath().equals("raw/test.html")) {
+ try {
return new ByteArrayInputStream(testHtmlContents.getBytes("utf-8"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
- } catch (IOException e) {
- Log.e(TAG, "exception when creating response", e);
}
return null;
}
@@ -237,7 +243,9 @@
WebResourceResponse response =
assetLoader.shouldInterceptRequest("http://example.com/android_res/raw/test.html");
- Assert.assertNotNull(response);
+ Assert.assertNotNull("failed to match the URL and returned null response", response);
+ Assert.assertNotNull("matched the prefix URL but not the file",
+ response.getData());
Assert.assertEquals(testHtmlContents, readAsString(response.getData(), "utf-8"));
}
}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
index 84f52f3..21e687c 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
@@ -30,6 +30,7 @@
import android.webkit.WebView;
import android.webkit.WebViewClient;
+import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
@@ -45,7 +46,7 @@
* Modifications to this class should be reflected in that class as necessary. See
* http://go/modifying-webview-cts.
*/
-class WebViewOnUiThread {
+public class WebViewOnUiThread {
/**
* The maximum time, in milliseconds (10 seconds) to wait for a load
* to be triggered.
@@ -69,10 +70,22 @@
private WebView mWebView;
public WebViewOnUiThread() {
+ this(WebkitUtils.onMainThreadSync(new Callable<WebView>() {
+ @Override
+ public WebView call() {
+ return new WebView(ApplicationProvider.getApplicationContext());
+ }
+ }));
+ }
+
+ /**
+ * Create a new WebViewOnUiThread wrapping the provided {@link WebView}.
+ */
+ public WebViewOnUiThread(final WebView webView) {
WebkitUtils.onMainThreadSync(new Runnable() {
@Override
public void run() {
- mWebView = new WebView(ApplicationProvider.getApplicationContext());
+ mWebView = webView;
mWebView.setWebViewClient(new WaitForLoadedClient(WebViewOnUiThread.this));
mWebView.setWebChromeClient(new WaitForProgressClient(WebViewOnUiThread.this));
}
@@ -520,6 +533,7 @@
}
@Override
+ @CallSuper
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
mOnUiThread.onProgressChanged(newProgress);
@@ -541,12 +555,14 @@
}
@Override
+ @CallSuper
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mOnUiThread.onPageFinished();
}
@Override
+ @CallSuper
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
mOnUiThread.onPageStarted();
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessTest.java
index c2fc7c9..fda6677 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewRenderProcessTest.java
@@ -31,6 +31,7 @@
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Assert;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -102,11 +103,24 @@
return future;
}
+ @Before
+ public void setUp() {
+ WebkitUtils.checkFeature(WebViewFeature.GET_WEB_VIEW_RENDERER);
+
+ // Ensure that any existing renderer still alive after a previous test is terminated.
+ // TODO(tobiasjs): This assumes that WebView uses at most one renderer, which is true
+ // for now but may not remain so in future.
+ final WebView webView = WebViewOnUiThread.createWebView();
+ final WebViewRenderProcess renderProcess = getRenderProcessOnUiThread(webView);
+ WebViewOnUiThread.destroy(webView);
+ if (renderProcess != null) {
+ terminateRenderProcessOnUiThread(renderProcess);
+ }
+ }
+
@Test
@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1)
public void testGetWebViewRenderProcessPreO() throws Throwable {
- WebkitUtils.checkFeature(WebViewFeature.GET_WEB_VIEW_RENDERER);
-
// It should not be possible to get a renderer pre-O
WebView webView = WebViewOnUiThread.createWebView();
final WebViewRenderProcess renderer = startAndGetRenderProcess(webView).get();
@@ -120,7 +134,6 @@
@SuppressLint("NewApi")
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
public void testGetWebViewRenderProcess() throws Throwable {
- WebkitUtils.checkFeature(WebViewFeature.GET_WEB_VIEW_RENDERER);
// TODO(tobiasjs) some O devices are not multiprocess, and multiprocess can also be disabled
// manually. This test should handle those scenarios.
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index a03c39c..c85c010 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -47,7 +47,7 @@
implementation(CONSTRAINT_LAYOUT, { transitive = true })
implementation project(':work:work-runtime-ktx')
- implementation(ARCH_CORE_RUNTIME)
+ implementation(WORK_ARCH_CORE_RUNTIME)
implementation(ARCH_LIFECYCLE_EXTENSIONS)
implementation(ANDROIDX_RECYCLERVIEW)
implementation(MATERIAL)
diff --git a/work/workmanager-testing/build.gradle b/work/workmanager-testing/build.gradle
index 13c5338..2bbc54f 100644
--- a/work/workmanager-testing/build.gradle
+++ b/work/workmanager-testing/build.gradle
@@ -31,7 +31,7 @@
implementation(ARCH_ROOM_RUNTIME)
annotationProcessor(ARCH_ROOM_COMPILER)
- androidTestImplementation(ARCH_CORE_TESTING)
+ androidTestImplementation(WORK_ARCH_CORE_TESTING)
androidTestImplementation(TEST_EXT_JUNIT)
androidTestImplementation(TEST_CORE)
androidTestImplementation(TEST_RUNNER)
@@ -40,6 +40,10 @@
androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
testImplementation(JUNIT)
+ testImplementation(ROBOLECTRIC)
+ testImplementation(TEST_EXT_JUNIT)
+ testImplementation(TEST_CORE)
+ testImplementation(TEST_RUNNER)
}
supportLibrary {
diff --git a/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java b/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
index 873ad26..0b39778 100644
--- a/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
+++ b/work/workmanager-testing/src/main/java/androidx/work/testing/TestWorkManagerImpl.java
@@ -22,19 +22,26 @@
import androidx.annotation.RestrictTo;
import androidx.work.Configuration;
import androidx.work.WorkManager;
+import androidx.work.impl.Scheduler;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
import java.util.concurrent.Executor;
/**
- * A concrete implementation of {@link WorkManager} which can be used for testing.
- * This implementation makes it easy to swap Schedulers.
+ * A concrete implementation of {@link WorkManager} which can be used for testing. This
+ * implementation makes it easy to swap Schedulers.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-abstract class TestWorkManagerImpl extends WorkManagerImpl implements TestDriver {
+class TestWorkManagerImpl extends WorkManagerImpl implements TestDriver {
+
+ private TestScheduler mScheduler;
+
TestWorkManagerImpl(
@NonNull Context context,
@NonNull Configuration configuration) {
@@ -75,5 +82,29 @@
}
},
true);
+
+ // mScheduler is initialized in createSchedulers() called by super()
+ getProcessor().addExecutionListener(mScheduler);
+ }
+
+ @Override
+ public @NonNull List<Scheduler> createSchedulers(Context context) {
+ mScheduler = new TestScheduler();
+ return Collections.singletonList((Scheduler) mScheduler);
+ }
+
+ @Override
+ public void setAllConstraintsMet(@NonNull UUID workSpecId) {
+ mScheduler.setAllConstraintsMet(workSpecId);
+ }
+
+ @Override
+ public void setInitialDelayMet(@NonNull UUID workSpecId) {
+ mScheduler.setInitialDelayMet(workSpecId);
+ }
+
+ @Override
+ public void setPeriodDelayMet(@NonNull UUID workSpecId) {
+ mScheduler.setPeriodDelayMet(workSpecId);
}
}
diff --git a/work/workmanager-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java b/work/workmanager-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
index 1e09b60..270e0d2 100644
--- a/work/workmanager-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
+++ b/work/workmanager-testing/src/main/java/androidx/work/testing/WorkManagerTestInitHelper.java
@@ -20,12 +20,8 @@
import androidx.annotation.NonNull;
import androidx.work.Configuration;
-import androidx.work.impl.Scheduler;
import androidx.work.impl.WorkManagerImpl;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
/**
* Helps initialize {@link androidx.work.WorkManager} for testing.
@@ -54,32 +50,7 @@
public static void initializeTestWorkManager(
@NonNull Context context,
@NonNull Configuration configuration) {
-
- final TestScheduler scheduler = new TestScheduler();
- WorkManagerImpl workManager = new TestWorkManagerImpl(context, configuration) {
-
- @Override
- public @NonNull List<Scheduler> createSchedulers(Context context) {
- return Collections.singletonList((Scheduler) scheduler);
- }
-
- @Override
- public void setAllConstraintsMet(@NonNull UUID workSpecId) {
- scheduler.setAllConstraintsMet(workSpecId);
- }
-
- @Override
- public void setInitialDelayMet(@NonNull UUID workSpecId) {
- scheduler.setInitialDelayMet(workSpecId);
- }
-
- @Override
- public void setPeriodDelayMet(@NonNull UUID workSpecId) {
- scheduler.setPeriodDelayMet(workSpecId);
- }
- };
- workManager.getProcessor().addExecutionListener(scheduler);
- WorkManagerImpl.setDelegate(workManager);
+ WorkManagerImpl.setDelegate(new TestWorkManagerImpl(context, configuration));
}
/**
diff --git a/work/workmanager-testing/src/test/java/androidx/work/testing/RobolectricSmokeTest.java b/work/workmanager-testing/src/test/java/androidx/work/testing/RobolectricSmokeTest.java
new file mode 100644
index 0000000..4671314
--- /dev/null
+++ b/work/workmanager-testing/src/test/java/androidx/work/testing/RobolectricSmokeTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 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 androidx.work.testing;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkRequest;
+import androidx.work.impl.WorkManagerImpl;
+import androidx.work.testing.workers.TestWorker;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+
+@Config(manifest = Config.NONE)
+@RunWith(RobolectricTestRunner.class)
+@LargeTest
+@DoNotInstrument
+public class RobolectricSmokeTest {
+ @Before
+ public void setUp() {
+ Context context = ApplicationProvider.getApplicationContext();
+ WorkManagerTestInitHelper.initializeTestWorkManager(context);
+ }
+
+ @Test
+ public void testWorker_shouldSucceedSynchronously()
+ throws InterruptedException, ExecutionException {
+ WorkRequest request = new OneTimeWorkRequest.Builder(TestWorker.class).build();
+ // TestWorkManagerImpl is a subtype of WorkManagerImpl.
+ WorkManagerImpl workManagerImpl = WorkManagerImpl.getInstance();
+ workManagerImpl.enqueue(Collections.singletonList(request)).getResult().get();
+ WorkInfo status = workManagerImpl.getWorkInfoById(request.getId()).get();
+ assertThat(status.getState().isFinished(), is(true));
+ }
+}
diff --git a/work/workmanager-testing/src/test/java/androidx/work/testing/workers/TestWorker.java b/work/workmanager-testing/src/test/java/androidx/work/testing/workers/TestWorker.java
new file mode 100644
index 0000000..78b7ab0
--- /dev/null
+++ b/work/workmanager-testing/src/test/java/androidx/work/testing/workers/TestWorker.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 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 androidx.work.testing.workers;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.work.Logger;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+/** A test {@link Worker} that prints a log and returns a successful result. */
+public class TestWorker extends Worker {
+ private static final String TAG = Logger.tagWithPrefix("TestWorker");
+
+ public TestWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @Override
+ public @NonNull Result doWork() {
+ Log.i(TAG, "Doing work.");
+ return Result.success();
+ }
+}
diff --git a/work/workmanager/build.gradle b/work/workmanager/build.gradle
index 4232d45..d424b64 100644
--- a/work/workmanager/build.gradle
+++ b/work/workmanager/build.gradle
@@ -58,7 +58,7 @@
api(GUAVA_LISTENABLE_FUTURE)
androidTestImplementation(TEST_EXT_JUNIT)
androidTestImplementation(TEST_CORE)
- androidTestImplementation(ARCH_CORE_TESTING)
+ androidTestImplementation(WORK_ARCH_CORE_TESTING)
androidTestImplementation(TEST_RUNNER)
androidTestImplementation(ESPRESSO_CORE)
androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has its own MockMaker