diff options
Diffstat (limited to 'android/arch/lifecycle')
38 files changed, 1804 insertions, 606 deletions
diff --git a/android/arch/lifecycle/ActivityFullLifecycleTest.java b/android/arch/lifecycle/ActivityFullLifecycleTest.java index ee4e661a..78dd0150 100644 --- a/android/arch/lifecycle/ActivityFullLifecycleTest.java +++ b/android/arch/lifecycle/ActivityFullLifecycleTest.java @@ -16,48 +16,43 @@ package android.arch.lifecycle; -import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE; -import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY; -import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE; -import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME; -import static android.arch.lifecycle.Lifecycle.Event.ON_START; -import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; -import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; -import static android.arch.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT; +import static android.arch.lifecycle.TestUtils.OrderedTuples.CREATE; +import static android.arch.lifecycle.TestUtils.OrderedTuples.DESTROY; +import static android.arch.lifecycle.TestUtils.OrderedTuples.PAUSE; +import static android.arch.lifecycle.TestUtils.OrderedTuples.RESUME; +import static android.arch.lifecycle.TestUtils.OrderedTuples.START; +import static android.arch.lifecycle.TestUtils.OrderedTuples.STOP; +import static android.arch.lifecycle.TestUtils.flatMap; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import android.app.Activity; import android.arch.lifecycle.Lifecycle.Event; -import android.arch.lifecycle.testapp.CollectingActivity; +import android.arch.lifecycle.testapp.CollectingLifecycleOwner; +import android.arch.lifecycle.testapp.CollectingSupportActivity; import android.arch.lifecycle.testapp.FrameworkLifecycleRegistryActivity; -import android.arch.lifecycle.testapp.FullLifecycleTestActivity; -import android.arch.lifecycle.testapp.SupportLifecycleRegistryActivity; import android.arch.lifecycle.testapp.TestEvent; import android.support.test.filters.SmallTest; import android.support.test.rule.ActivityTestRule; -import android.util.Pair; +import android.support.v4.util.Pair; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import java.util.ArrayList; import java.util.List; @SmallTest @RunWith(Parameterized.class) public class ActivityFullLifecycleTest { @Rule - public ActivityTestRule activityTestRule = - new ActivityTestRule<>(FullLifecycleTestActivity.class); + public final ActivityTestRule<? extends CollectingLifecycleOwner> activityTestRule; @Parameterized.Parameters public static Class[] params() { - return new Class[]{FullLifecycleTestActivity.class, - SupportLifecycleRegistryActivity.class, + return new Class[]{CollectingSupportActivity.class, FrameworkLifecycleRegistryActivity.class}; } @@ -68,28 +63,13 @@ public class ActivityFullLifecycleTest { @Test - public void testFullLifecycle() throws InterruptedException { - Activity activity = activityTestRule.getActivity(); - List<Pair<TestEvent, Event>> results = ((CollectingActivity) activity) - .waitForCollectedEvents(); + public void testFullLifecycle() throws Throwable { + CollectingLifecycleOwner owner = activityTestRule.getActivity(); + TestUtils.waitTillResumed(owner, activityTestRule); + activityTestRule.finishActivity(); - Event[] expectedEvents = - new Event[]{ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY}; - - List<Pair<TestEvent, Event>> expected = new ArrayList<>(); - boolean beforeResume = true; - for (Event i : expectedEvents) { - if (beforeResume) { - expected.add(new Pair<>(ACTIVITY_CALLBACK, i)); - expected.add(new Pair<>(LIFECYCLE_EVENT, i)); - } else { - expected.add(new Pair<>(LIFECYCLE_EVENT, i)); - expected.add(new Pair<>(ACTIVITY_CALLBACK, i)); - } - if (i == ON_RESUME) { - beforeResume = false; - } - } - assertThat(results, is(expected)); + TestUtils.waitTillDestroyed(owner, activityTestRule); + List<Pair<TestEvent, Event>> results = owner.copyCollectedEvents(); + assertThat(results, is(flatMap(CREATE, START, RESUME, PAUSE, STOP, DESTROY))); } } diff --git a/android/arch/lifecycle/AndroidViewModel.java b/android/arch/lifecycle/AndroidViewModel.java index 2c7e1739..106b2ef0 100644 --- a/android/arch/lifecycle/AndroidViewModel.java +++ b/android/arch/lifecycle/AndroidViewModel.java @@ -16,7 +16,9 @@ package android.arch.lifecycle; +import android.annotation.SuppressLint; import android.app.Application; +import android.support.annotation.NonNull; /** * Application context aware {@link ViewModel}. @@ -25,16 +27,19 @@ import android.app.Application; * <p> */ public class AndroidViewModel extends ViewModel { + @SuppressLint("StaticFieldLeak") private Application mApplication; - public AndroidViewModel(Application application) { + public AndroidViewModel(@NonNull Application application) { mApplication = application; } /** * Return the application. */ + @NonNull public <T extends Application> T getApplication() { + //noinspection unchecked return (T) mApplication; } } diff --git a/android/arch/lifecycle/ClassesInfoCache.java b/android/arch/lifecycle/ClassesInfoCache.java index f077daed..d88e2762 100644 --- a/android/arch/lifecycle/ClassesInfoCache.java +++ b/android/arch/lifecycle/ClassesInfoCache.java @@ -46,7 +46,7 @@ class ClassesInfoCache { return mHasLifecycleMethods.get(klass); } - Method[] methods = klass.getDeclaredMethods(); + Method[] methods = getDeclaredMethods(klass); for (Method method : methods) { OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class); if (annotation != null) { @@ -64,6 +64,18 @@ class ClassesInfoCache { return false; } + private Method[] getDeclaredMethods(Class klass) { + try { + return klass.getDeclaredMethods(); + } catch (NoClassDefFoundError e) { + throw new IllegalArgumentException("The observer class has some methods that use " + + "newer APIs which are not available in the current OS version. Lifecycles " + + "cannot access even other methods so you should make sure that your " + + "observer classes only access framework classes that are available " + + "in your min API level OR use lifecycle:compiler annotation processor.", e); + } + } + CallbackInfo getInfo(Class klass) { CallbackInfo existing = mCallbackMap.get(klass); if (existing != null) { @@ -106,7 +118,7 @@ class ClassesInfoCache { } } - Method[] methods = declaredMethods != null ? declaredMethods : klass.getDeclaredMethods(); + Method[] methods = declaredMethods != null ? declaredMethods : getDeclaredMethods(klass); boolean hasLifecycleMethods = false; for (Method method : methods) { OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class); diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java index f1352446..1ddcb1a9 100644 --- a/android/arch/lifecycle/ComputableLiveData.java +++ b/android/arch/lifecycle/ComputableLiveData.java @@ -1,9 +1,136 @@ -//ComputableLiveData interface for tests +/* + * 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 android.arch.lifecycle; -import android.arch.lifecycle.LiveData; + +import android.arch.core.executor.ArchTaskExecutor; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A LiveData class that can be invalidated & computed on demand. + * <p> + * This is an internal class for now, might be public if we see the necessity. + * + * @param <T> The type of the live data + * @hide internal + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class ComputableLiveData<T> { - public ComputableLiveData(){} - abstract protected T compute(); - public LiveData<T> getLiveData() {return null;} - public void invalidate() {} + + private final LiveData<T> mLiveData; + + private AtomicBoolean mInvalid = new AtomicBoolean(true); + private AtomicBoolean mComputing = new AtomicBoolean(false); + + /** + * Creates a computable live data which is computed when there are active observers. + * <p> + * It can also be invalidated via {@link #invalidate()} which will result in a call to + * {@link #compute()} if there are active observers (or when they start observing) + */ + @SuppressWarnings("WeakerAccess") + public ComputableLiveData() { + mLiveData = new LiveData<T>() { + @Override + protected void onActive() { + // TODO if we make this class public, we should accept an executor + ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); + } + }; + } + + /** + * Returns the LiveData managed by this class. + * + * @return A LiveData that is controlled by ComputableLiveData. + */ + @SuppressWarnings("WeakerAccess") + @NonNull + public LiveData<T> getLiveData() { + return mLiveData; + } + + @VisibleForTesting + final Runnable mRefreshRunnable = new Runnable() { + @WorkerThread + @Override + public void run() { + boolean computed; + do { + computed = false; + // compute can happen only in 1 thread but no reason to lock others. + if (mComputing.compareAndSet(false, true)) { + // as long as it is invalid, keep computing. + try { + T value = null; + while (mInvalid.compareAndSet(true, false)) { + computed = true; + value = compute(); + } + if (computed) { + mLiveData.postValue(value); + } + } finally { + // release compute lock + mComputing.set(false); + } + } + // check invalid after releasing compute lock to avoid the following scenario. + // Thread A runs compute() + // Thread A checks invalid, it is false + // Main thread sets invalid to true + // Thread B runs, fails to acquire compute lock and skips + // Thread A releases compute lock + // We've left invalid in set state. The check below recovers. + } while (computed && mInvalid.get()); + } + }; + + // invalidation check always happens on the main thread + @VisibleForTesting + final Runnable mInvalidationRunnable = new Runnable() { + @MainThread + @Override + public void run() { + boolean isActive = mLiveData.hasActiveObservers(); + if (mInvalid.compareAndSet(false, true)) { + if (isActive) { + // TODO if we make this class public, we should accept an executor. + ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable); + } + } + } + }; + + /** + * Invalidates the LiveData. + * <p> + * When there are active observers, this will trigger a call to {@link #compute()}. + */ + public void invalidate() { + ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable); + } + + @SuppressWarnings("WeakerAccess") + @WorkerThread + protected abstract T compute(); } diff --git a/android/arch/lifecycle/DispatcherActivityCallbackTest.java b/android/arch/lifecycle/DispatcherActivityCallbackTest.java deleted file mode 100644 index 86b25b60..00000000 --- a/android/arch/lifecycle/DispatcherActivityCallbackTest.java +++ /dev/null @@ -1,77 +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 android.arch.lifecycle; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Fragment; -import android.app.FragmentTransaction; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentManager; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class DispatcherActivityCallbackTest { - @Test - public void onCreateFrameworkActivity() { - LifecycleDispatcher.DispatcherActivityCallback callback = - new LifecycleDispatcher.DispatcherActivityCallback(); - Activity activity = mock(Activity.class); - checkReportFragment(callback, activity); - } - - @Test - public void onCreateFragmentActivity() { - LifecycleDispatcher.DispatcherActivityCallback callback = - new LifecycleDispatcher.DispatcherActivityCallback(); - FragmentActivity activity = mock(FragmentActivity.class); - FragmentManager fragmentManager = mock(FragmentManager.class); - when(activity.getSupportFragmentManager()).thenReturn(fragmentManager); - - checkReportFragment(callback, activity); - - verify(activity).getSupportFragmentManager(); - verify(fragmentManager).registerFragmentLifecycleCallbacks( - any(FragmentManager.FragmentLifecycleCallbacks.class), eq(true)); - } - - @SuppressLint("CommitTransaction") - private void checkReportFragment(LifecycleDispatcher.DispatcherActivityCallback callback, - Activity activity) { - android.app.FragmentManager fm = mock(android.app.FragmentManager.class); - FragmentTransaction transaction = mock(FragmentTransaction.class); - when(activity.getFragmentManager()).thenReturn(fm); - when(fm.beginTransaction()).thenReturn(transaction); - when(transaction.add(any(Fragment.class), anyString())).thenReturn(transaction); - callback.onActivityCreated(activity, mock(Bundle.class)); - verify(activity).getFragmentManager(); - verify(fm).beginTransaction(); - verify(transaction).add(any(ReportFragment.class), anyString()); - verify(transaction).commit(); - } -} diff --git a/android/arch/lifecycle/Lifecycle.java b/android/arch/lifecycle/Lifecycle.java index 02db5ff9..c0a2090c 100644 --- a/android/arch/lifecycle/Lifecycle.java +++ b/android/arch/lifecycle/Lifecycle.java @@ -17,6 +17,7 @@ package android.arch.lifecycle; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; /** * Defines an object that has an Android Lifecycle. {@link android.support.v4.app.Fragment Fragment} @@ -83,7 +84,7 @@ public abstract class Lifecycle { * @param observer The observer to notify. */ @MainThread - public abstract void addObserver(LifecycleObserver observer); + public abstract void addObserver(@NonNull LifecycleObserver observer); /** * Removes the given observer from the observers list. @@ -99,7 +100,7 @@ public abstract class Lifecycle { * @param observer The observer to be removed. */ @MainThread - public abstract void removeObserver(LifecycleObserver observer); + public abstract void removeObserver(@NonNull LifecycleObserver observer); /** * Returns the current state of the Lifecycle. @@ -193,7 +194,7 @@ public abstract class Lifecycle { * @param state State to compare with * @return true if this State is greater or equal to the given {@code state} */ - public boolean isAtLeast(State state) { + public boolean isAtLeast(@NonNull State state) { return compareTo(state) >= 0; } } diff --git a/android/arch/lifecycle/LifecycleDispatcher.java b/android/arch/lifecycle/LifecycleDispatcher.java deleted file mode 100644 index 9fdec959..00000000 --- a/android/arch/lifecycle/LifecycleDispatcher.java +++ /dev/null @@ -1,182 +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 android.arch.lifecycle; - -import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE; -import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY; -import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE; -import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME; -import static android.arch.lifecycle.Lifecycle.Event.ON_START; -import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; -import static android.arch.lifecycle.Lifecycle.State.CREATED; - -import android.app.Activity; -import android.app.Application; -import android.arch.lifecycle.Lifecycle.State; -import android.content.Context; -import android.os.Bundle; -import android.support.annotation.VisibleForTesting; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentManager; - -import java.util.Collection; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * When initialized, it hooks into the Activity callback of the Application and observes - * Activities. It is responsible to hook in child-fragments to activities and fragments to report - * their lifecycle events. Another responsibility of this class is to mark as stopped all lifecycle - * providers related to an activity as soon it is not safe to run a fragment transaction in this - * activity. - */ -class LifecycleDispatcher { - - private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle" - + ".LifecycleDispatcher.report_fragment_tag"; - - private static AtomicBoolean sInitialized = new AtomicBoolean(false); - - static void init(Context context) { - if (sInitialized.getAndSet(true)) { - return; - } - ((Application) context.getApplicationContext()) - .registerActivityLifecycleCallbacks(new DispatcherActivityCallback()); - } - - @SuppressWarnings("WeakerAccess") - @VisibleForTesting - static class DispatcherActivityCallback extends EmptyActivityLifecycleCallbacks { - private final FragmentCallback mFragmentCallback; - - DispatcherActivityCallback() { - mFragmentCallback = new FragmentCallback(); - } - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - if (activity instanceof FragmentActivity) { - ((FragmentActivity) activity).getSupportFragmentManager() - .registerFragmentLifecycleCallbacks(mFragmentCallback, true); - } - ReportFragment.injectIfNeededIn(activity); - } - - @Override - public void onActivityStopped(Activity activity) { - if (activity instanceof FragmentActivity) { - markState((FragmentActivity) activity, CREATED); - } - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - if (activity instanceof FragmentActivity) { - markState((FragmentActivity) activity, CREATED); - } - } - } - - @SuppressWarnings("WeakerAccess") - public static class DestructionReportFragment extends Fragment { - @Override - public void onPause() { - super.onPause(); - dispatch(ON_PAUSE); - } - - @Override - public void onStop() { - super.onStop(); - dispatch(ON_STOP); - } - - @Override - public void onDestroy() { - super.onDestroy(); - dispatch(ON_DESTROY); - } - - protected void dispatch(Lifecycle.Event event) { - dispatchIfLifecycleOwner(getParentFragment(), event); - } - } - - private static void markState(FragmentManager manager, State state) { - Collection<Fragment> fragments = manager.getFragments(); - if (fragments == null) { - return; - } - for (Fragment fragment : fragments) { - if (fragment == null) { - continue; - } - markStateIn(fragment, state); - if (fragment.isAdded()) { - markState(fragment.getChildFragmentManager(), state); - } - } - } - - private static void markStateIn(Object object, State state) { - if (object instanceof LifecycleRegistryOwner) { - LifecycleRegistry registry = ((LifecycleRegistryOwner) object).getLifecycle(); - registry.markState(state); - } - } - - private static void markState(FragmentActivity activity, State state) { - markStateIn(activity, state); - markState(activity.getSupportFragmentManager(), state); - } - - private static void dispatchIfLifecycleOwner(Fragment fragment, Lifecycle.Event event) { - if (fragment instanceof LifecycleRegistryOwner) { - ((LifecycleRegistryOwner) fragment).getLifecycle().handleLifecycleEvent(event); - } - } - - @SuppressWarnings("WeakerAccess") - @VisibleForTesting - static class FragmentCallback extends FragmentManager.FragmentLifecycleCallbacks { - - @Override - public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) { - dispatchIfLifecycleOwner(f, ON_CREATE); - - if (!(f instanceof LifecycleRegistryOwner)) { - return; - } - - if (f.getChildFragmentManager().findFragmentByTag(REPORT_FRAGMENT_TAG) == null) { - f.getChildFragmentManager().beginTransaction().add(new DestructionReportFragment(), - REPORT_FRAGMENT_TAG).commit(); - } - } - - @Override - public void onFragmentStarted(FragmentManager fm, Fragment f) { - dispatchIfLifecycleOwner(f, ON_START); - } - - @Override - public void onFragmentResumed(FragmentManager fm, Fragment f) { - dispatchIfLifecycleOwner(f, ON_RESUME); - } - } -} diff --git a/android/arch/lifecycle/LifecycleOwner.java b/android/arch/lifecycle/LifecycleOwner.java index 934cf3a2..068bac1b 100644 --- a/android/arch/lifecycle/LifecycleOwner.java +++ b/android/arch/lifecycle/LifecycleOwner.java @@ -16,6 +16,8 @@ package android.arch.lifecycle; +import android.support.annotation.NonNull; + /** * A class that has an Android lifecycle. These events can be used by custom components to * handle lifecycle changes without implementing any code inside the Activity or the Fragment. @@ -29,5 +31,6 @@ public interface LifecycleOwner { * * @return The lifecycle of the provider. */ + @NonNull Lifecycle getLifecycle(); } diff --git a/android/arch/lifecycle/LifecycleRegistry.java b/android/arch/lifecycle/LifecycleRegistry.java index b83e6b8a..bf8aff79 100644 --- a/android/arch/lifecycle/LifecycleRegistry.java +++ b/android/arch/lifecycle/LifecycleRegistry.java @@ -29,9 +29,12 @@ import static android.arch.lifecycle.Lifecycle.State.RESUMED; import static android.arch.lifecycle.Lifecycle.State.STARTED; import android.arch.core.internal.FastSafeIterableMap; +import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Log; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Iterator; import java.util.Map.Entry; @@ -44,6 +47,8 @@ import java.util.Map.Entry; */ public class LifecycleRegistry extends Lifecycle { + private static final String LOG_TAG = "LifecycleRegistry"; + /** * Custom list that keeps observers and can handle removals / additions during traversal. * @@ -59,8 +64,12 @@ public class LifecycleRegistry extends Lifecycle { private State mState; /** * The provider that owns this Lifecycle. + * Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak + * the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither, + * because it keeps strong references on all other listeners, so you'll leak all of them as + * well. */ - private final LifecycleOwner mLifecycleOwner; + private final WeakReference<LifecycleOwner> mLifecycleOwner; private int mAddingObserverCounter = 0; @@ -86,19 +95,19 @@ public class LifecycleRegistry extends Lifecycle { * @param provider The owner LifecycleOwner */ public LifecycleRegistry(@NonNull LifecycleOwner provider) { - mLifecycleOwner = provider; + mLifecycleOwner = new WeakReference<>(provider); mState = INITIALIZED; } /** - * Only marks the current state as the given value. It doesn't dispatch any event to its - * listeners. + * Moves the Lifecycle to the given state and dispatches necessary events to the observers. * * @param state new state */ @SuppressWarnings("WeakerAccess") - public void markState(State state) { - mState = state; + @MainThread + public void markState(@NonNull State state) { + moveToState(state); } /** @@ -109,8 +118,16 @@ public class LifecycleRegistry extends Lifecycle { * * @param event The event that was received */ - public void handleLifecycleEvent(Lifecycle.Event event) { - mState = getStateAfter(event); + public void handleLifecycleEvent(@NonNull Lifecycle.Event event) { + State next = getStateAfter(event); + moveToState(next); + } + + private void moveToState(State next) { + if (mState == next) { + return; + } + mState = next; if (mHandlingEvent || mAddingObserverCounter != 0) { mNewEventOccurred = true; // we will figure out what to do on upper level. @@ -140,7 +157,7 @@ public class LifecycleRegistry extends Lifecycle { } @Override - public void addObserver(LifecycleObserver observer) { + public void addObserver(@NonNull LifecycleObserver observer) { State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED; ObserverWithState statefulObserver = new ObserverWithState(observer, initialState); ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver); @@ -148,15 +165,19 @@ public class LifecycleRegistry extends Lifecycle { if (previous != null) { return; } + LifecycleOwner lifecycleOwner = mLifecycleOwner.get(); + if (lifecycleOwner == null) { + // it is null we should be destroyed. Fallback quickly + return; + } boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent; - State targetState = calculateTargetState(observer); mAddingObserverCounter++; while ((statefulObserver.mState.compareTo(targetState) < 0 && mObserverMap.contains(observer))) { pushParentState(statefulObserver.mState); - statefulObserver.dispatchEvent(mLifecycleOwner, upEvent(statefulObserver.mState)); + statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState)); popParentState(); // mState / subling may have been changed recalculate targetState = calculateTargetState(observer); @@ -178,7 +199,7 @@ public class LifecycleRegistry extends Lifecycle { } @Override - public void removeObserver(LifecycleObserver observer) { + public void removeObserver(@NonNull LifecycleObserver observer) { // we consciously decided not to send destruction events here in opposition to addObserver. // Our reasons for that: // 1. These events haven't yet happened at all. In contrast to events in addObservers, that @@ -258,7 +279,7 @@ public class LifecycleRegistry extends Lifecycle { throw new IllegalArgumentException("Unexpected state value " + state); } - private void forwardPass() { + private void forwardPass(LifecycleOwner lifecycleOwner) { Iterator<Entry<LifecycleObserver, ObserverWithState>> ascendingIterator = mObserverMap.iteratorWithAdditions(); while (ascendingIterator.hasNext() && !mNewEventOccurred) { @@ -267,13 +288,13 @@ public class LifecycleRegistry extends Lifecycle { while ((observer.mState.compareTo(mState) < 0 && !mNewEventOccurred && mObserverMap.contains(entry.getKey()))) { pushParentState(observer.mState); - observer.dispatchEvent(mLifecycleOwner, upEvent(observer.mState)); + observer.dispatchEvent(lifecycleOwner, upEvent(observer.mState)); popParentState(); } } } - private void backwardPass() { + private void backwardPass(LifecycleOwner lifecycleOwner) { Iterator<Entry<LifecycleObserver, ObserverWithState>> descendingIterator = mObserverMap.descendingIterator(); while (descendingIterator.hasNext() && !mNewEventOccurred) { @@ -283,7 +304,7 @@ public class LifecycleRegistry extends Lifecycle { && mObserverMap.contains(entry.getKey()))) { Event event = downEvent(observer.mState); pushParentState(getStateAfter(event)); - observer.dispatchEvent(mLifecycleOwner, event); + observer.dispatchEvent(lifecycleOwner, event); popParentState(); } } @@ -292,16 +313,22 @@ public class LifecycleRegistry extends Lifecycle { // happens only on the top of stack (never in reentrance), // so it doesn't have to take in account parents private void sync() { + LifecycleOwner lifecycleOwner = mLifecycleOwner.get(); + if (lifecycleOwner == null) { + Log.w(LOG_TAG, "LifecycleOwner is garbage collected, you shouldn't try dispatch " + + "new events from it."); + return; + } while (!isSynced()) { mNewEventOccurred = false; // no need to check eldest for nullability, because isSynced does it for us. if (mState.compareTo(mObserverMap.eldest().getValue().mState) < 0) { - backwardPass(); + backwardPass(lifecycleOwner); } Entry<LifecycleObserver, ObserverWithState> newest = mObserverMap.newest(); if (!mNewEventOccurred && newest != null && mState.compareTo(newest.getValue().mState) > 0) { - forwardPass(); + forwardPass(lifecycleOwner); } } mNewEventOccurred = false; diff --git a/android/arch/lifecycle/LifecycleRegistryOwner.java b/android/arch/lifecycle/LifecycleRegistryOwner.java index 38eeb6d3..0c67fefe 100644 --- a/android/arch/lifecycle/LifecycleRegistryOwner.java +++ b/android/arch/lifecycle/LifecycleRegistryOwner.java @@ -16,6 +16,8 @@ package android.arch.lifecycle; +import android.support.annotation.NonNull; + /** * @deprecated Use {@code android.support.v7.app.AppCompatActivity} * which extends {@link LifecycleOwner}, so there are no use cases for this class. @@ -23,6 +25,7 @@ package android.arch.lifecycle; @SuppressWarnings({"WeakerAccess", "unused"}) @Deprecated public interface LifecycleRegistryOwner extends LifecycleOwner { + @NonNull @Override LifecycleRegistry getLifecycle(); } diff --git a/android/arch/lifecycle/LifecycleRegistryTest.java b/android/arch/lifecycle/LifecycleRegistryTest.java index 6506454d..2a7bbad2 100644 --- a/android/arch/lifecycle/LifecycleRegistryTest.java +++ b/android/arch/lifecycle/LifecycleRegistryTest.java @@ -566,6 +566,25 @@ public class LifecycleRegistryTest { verify(observer).onCreate(); } + private static void forceGc() { + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + } + + @Test + public void goneLifecycleOwner() { + fullyInitializeRegistry(); + mLifecycleOwner = null; + forceGc(); + TestObserver observer = mock(TestObserver.class); + mRegistry.addObserver(observer); + verify(observer, never()).onCreate(); + verify(observer, never()).onStart(); + verify(observer, never()).onResume(); + } + private void dispatchEvent(Lifecycle.Event event) { when(mLifecycle.getCurrentState()).thenReturn(LifecycleRegistry.getStateAfter(event)); mRegistry.handleLifecycleEvent(event); diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java index 3aea6acb..5b09c32f 100644 --- a/android/arch/lifecycle/LiveData.java +++ b/android/arch/lifecycle/LiveData.java @@ -1,4 +1,410 @@ -//LiveData interface for tests +/* + * 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 android.arch.lifecycle; -public class LiveData<T> { + +import static android.arch.lifecycle.Lifecycle.State.DESTROYED; +import static android.arch.lifecycle.Lifecycle.State.STARTED; + +import android.arch.core.executor.ArchTaskExecutor; +import android.arch.core.internal.SafeIterableMap; +import android.arch.lifecycle.Lifecycle.State; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Iterator; +import java.util.Map; + +/** + * LiveData is a data holder class that can be observed within a given lifecycle. + * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and + * this observer will be notified about modifications of the wrapped data only if the paired + * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is + * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via + * {@link #observeForever(Observer)} is considered as always active and thus will be always notified + * about modifications. For those observers, you should manually call + * {@link #removeObserver(Observer)}. + * + * <p> An observer added with a Lifecycle will be automatically removed if the corresponding + * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for + * activities and fragments where they can safely observe LiveData and not worry about leaks: + * they will be instantly unsubscribed when they are destroyed. + * + * <p> + * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods + * to get notified when number of active {@link Observer}s change between 0 and 1. + * This allows LiveData to release any heavy resources when it does not have any Observers that + * are actively observing. + * <p> + * This class is designed to hold individual data fields of {@link ViewModel}, + * but can also be used for sharing data between different modules in your application + * in a decoupled fashion. + * + * @param <T> The type of data held by this instance + * @see ViewModel + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +// TODO: Thread checks are too strict right now, we may consider automatically moving them to main +// thread. +public abstract class LiveData<T> { + private final Object mDataLock = new Object(); + static final int START_VERSION = -1; + private static final Object NOT_SET = new Object(); + + private static final LifecycleOwner ALWAYS_ON = new LifecycleOwner() { + + private LifecycleRegistry mRegistry = init(); + + private LifecycleRegistry init() { + LifecycleRegistry registry = new LifecycleRegistry(this); + registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + registry.handleLifecycleEvent(Lifecycle.Event.ON_START); + registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + return registry; + } + + @Override + public Lifecycle getLifecycle() { + return mRegistry; + } + }; + + private SafeIterableMap<Observer<T>, LifecycleBoundObserver> mObservers = + new SafeIterableMap<>(); + + // how many observers are in active state + private int mActiveCount = 0; + private volatile Object mData = NOT_SET; + // when setData is called, we set the pending data and actual data swap happens on the main + // thread + private volatile Object mPendingData = NOT_SET; + private int mVersion = START_VERSION; + + private boolean mDispatchingValue; + @SuppressWarnings("FieldCanBeLocal") + private boolean mDispatchInvalidated; + private final Runnable mPostValueRunnable = new Runnable() { + @Override + public void run() { + Object newValue; + synchronized (mDataLock) { + newValue = mPendingData; + mPendingData = NOT_SET; + } + //noinspection unchecked + setValue((T) newValue); + } + }; + + private void considerNotify(LifecycleBoundObserver observer) { + if (!observer.active) { + return; + } + // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet. + // + // we still first check observer.active to keep it as the entrance for events. So even if + // the observer moved to an active state, if we've not received that event, we better not + // notify for a more predictable notification order. + if (!isActiveState(observer.owner.getLifecycle().getCurrentState())) { + observer.activeStateChanged(false); + return; + } + if (observer.lastVersion >= mVersion) { + return; + } + observer.lastVersion = mVersion; + //noinspection unchecked + observer.observer.onChanged((T) mData); + } + + private void dispatchingValue(@Nullable LifecycleBoundObserver initiator) { + if (mDispatchingValue) { + mDispatchInvalidated = true; + return; + } + mDispatchingValue = true; + do { + mDispatchInvalidated = false; + if (initiator != null) { + considerNotify(initiator); + initiator = null; + } else { + for (Iterator<Map.Entry<Observer<T>, LifecycleBoundObserver>> iterator = + mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { + considerNotify(iterator.next().getValue()); + if (mDispatchInvalidated) { + break; + } + } + } + } while (mDispatchInvalidated); + mDispatchingValue = false; + } + + /** + * Adds the given observer to the observers list within the lifespan of the given + * owner. The events are dispatched on the main thread. If LiveData already has data + * set, it will be delivered to the observer. + * <p> + * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED} + * or {@link Lifecycle.State#RESUMED} state (active). + * <p> + * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will + * automatically be removed. + * <p> + * When data changes while the {@code owner} is not active, it will not receive any updates. + * If it becomes active again, it will receive the last available data automatically. + * <p> + * LiveData keeps a strong reference to the observer and the owner as long as the + * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to + * the observer & the owner. + * <p> + * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData + * ignores the call. + * <p> + * If the given owner, observer tuple is already in the list, the call is ignored. + * If the observer is already in the list with another owner, LiveData throws an + * {@link IllegalArgumentException}. + * + * @param owner The LifecycleOwner which controls the observer + * @param observer The observer that will receive the events + */ + @MainThread + public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) { + if (owner.getLifecycle().getCurrentState() == DESTROYED) { + // ignore + return; + } + LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); + LifecycleBoundObserver existing = mObservers.putIfAbsent(observer, wrapper); + if (existing != null && existing.owner != wrapper.owner) { + throw new IllegalArgumentException("Cannot add the same observer" + + " with different lifecycles"); + } + if (existing != null) { + return; + } + owner.getLifecycle().addObserver(wrapper); + } + + /** + * Adds the given observer to the observers list. This call is similar to + * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which + * is always active. This means that the given observer will receive all events and will never + * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop + * observing this LiveData. + * While LiveData has one of such observers, it will be considered + * as active. + * <p> + * If the observer was already added with an owner to this LiveData, LiveData throws an + * {@link IllegalArgumentException}. + * + * @param observer The observer that will receive the events + */ + @MainThread + public void observeForever(@NonNull Observer<T> observer) { + observe(ALWAYS_ON, observer); + } + + /** + * Removes the given observer from the observers list. + * + * @param observer The Observer to receive events. + */ + @MainThread + public void removeObserver(@NonNull final Observer<T> observer) { + assertMainThread("removeObserver"); + LifecycleBoundObserver removed = mObservers.remove(observer); + if (removed == null) { + return; + } + removed.owner.getLifecycle().removeObserver(removed); + removed.activeStateChanged(false); + } + + /** + * Removes all observers that are tied to the given {@link LifecycleOwner}. + * + * @param owner The {@code LifecycleOwner} scope for the observers to be removed. + */ + @MainThread + public void removeObservers(@NonNull final LifecycleOwner owner) { + assertMainThread("removeObservers"); + for (Map.Entry<Observer<T>, LifecycleBoundObserver> entry : mObservers) { + if (entry.getValue().owner == owner) { + removeObserver(entry.getKey()); + } + } + } + + /** + * Posts a task to a main thread to set the given value. So if you have a following code + * executed in the main thread: + * <pre class="prettyprint"> + * liveData.postValue("a"); + * liveData.setValue("b"); + * </pre> + * The value "b" would be set at first and later the main thread would override it with + * the value "a". + * <p> + * If you called this method multiple times before a main thread executed a posted task, only + * the last value would be dispatched. + * + * @param value The new value + */ + protected void postValue(T value) { + boolean postTask; + synchronized (mDataLock) { + postTask = mPendingData == NOT_SET; + mPendingData = value; + } + if (!postTask) { + return; + } + ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); + } + + /** + * Sets the value. If there are active observers, the value will be dispatched to them. + * <p> + * This method must be called from the main thread. If you need set a value from a background + * thread, you can use {@link #postValue(Object)} + * + * @param value The new value + */ + @MainThread + protected void setValue(T value) { + assertMainThread("setValue"); + mVersion++; + mData = value; + dispatchingValue(null); + } + + /** + * Returns the current value. + * Note that calling this method on a background thread does not guarantee that the latest + * value set will be received. + * + * @return the current value + */ + @Nullable + public T getValue() { + Object data = mData; + if (data != NOT_SET) { + //noinspection unchecked + return (T) data; + } + return null; + } + + int getVersion() { + return mVersion; + } + + /** + * Called when the number of active observers change to 1 from 0. + * <p> + * This callback can be used to know that this LiveData is being used thus should be kept + * up to date. + */ + protected void onActive() { + + } + + /** + * Called when the number of active observers change from 1 to 0. + * <p> + * This does not mean that there are no observers left, there may still be observers but their + * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED} + * (like an Activity in the back stack). + * <p> + * You can check if there are observers via {@link #hasObservers()}. + */ + protected void onInactive() { + + } + + /** + * Returns true if this LiveData has observers. + * + * @return true if this LiveData has observers + */ + public boolean hasObservers() { + return mObservers.size() > 0; + } + + /** + * Returns true if this LiveData has active observers. + * + * @return true if this LiveData has active observers + */ + public boolean hasActiveObservers() { + return mActiveCount > 0; + } + + class LifecycleBoundObserver implements GenericLifecycleObserver { + public final LifecycleOwner owner; + public final Observer<T> observer; + public boolean active; + public int lastVersion = START_VERSION; + + LifecycleBoundObserver(LifecycleOwner owner, Observer<T> observer) { + this.owner = owner; + this.observer = observer; + } + + @Override + public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { + if (owner.getLifecycle().getCurrentState() == DESTROYED) { + removeObserver(observer); + return; + } + // immediately set active state, so we'd never dispatch anything to inactive + // owner + activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState())); + } + + void activeStateChanged(boolean newActive) { + if (newActive == active) { + return; + } + active = newActive; + boolean wasInactive = LiveData.this.mActiveCount == 0; + LiveData.this.mActiveCount += active ? 1 : -1; + if (wasInactive && active) { + onActive(); + } + if (LiveData.this.mActiveCount == 0 && !active) { + onInactive(); + } + if (active) { + dispatchingValue(this); + } + } + } + + static boolean isActiveState(State state) { + return state.isAtLeast(STARTED); + } + + private void assertMainThread(String methodName) { + if (!ArchTaskExecutor.getInstance().isMainThread()) { + throw new IllegalStateException("Cannot invoke " + methodName + " on a background" + + " thread"); + } + } } diff --git a/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.java b/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.java new file mode 100644 index 00000000..836cfff0 --- /dev/null +++ b/android/arch/lifecycle/LiveDataOnSaveInstanceStateTest.java @@ -0,0 +1,177 @@ +/* + * 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 android.arch.lifecycle; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import android.app.Instrumentation; +import android.arch.lifecycle.testapp.CollectingSupportActivity; +import android.arch.lifecycle.testapp.CollectingSupportFragment; +import android.arch.lifecycle.testapp.NavigationDialogActivity; +import android.content.Intent; +import android.os.Build; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.v4.app.FragmentActivity; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.atomic.AtomicInteger; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class LiveDataOnSaveInstanceStateTest { + @Rule + public ActivityTestRule<CollectingSupportActivity> mActivityTestRule = + new ActivityTestRule<>(CollectingSupportActivity.class); + + @Test + @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M) + public void liveData_partiallyObscuredActivity_maxSdkM() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + + liveData_partiallyObscuredLifecycleOwner_maxSdkM(activity); + } + + @Test + @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M) + public void liveData_partiallyObscuredActivityWithFragment_maxSdkM() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + CollectingSupportFragment fragment = new CollectingSupportFragment(); + mActivityTestRule.runOnUiThread(() -> activity.replaceFragment(fragment)); + + liveData_partiallyObscuredLifecycleOwner_maxSdkM(fragment); + } + + @Test + @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M) + public void liveData_partiallyObscuredActivityFragmentInFragment_maxSdkM() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + CollectingSupportFragment fragment = new CollectingSupportFragment(); + CollectingSupportFragment fragment2 = new CollectingSupportFragment(); + mActivityTestRule.runOnUiThread(() -> { + activity.replaceFragment(fragment); + fragment.replaceFragment(fragment2); + }); + + liveData_partiallyObscuredLifecycleOwner_maxSdkM(fragment2); + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) + public void liveData_partiallyObscuredActivity_minSdkN() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + + liveData_partiallyObscuredLifecycleOwner_minSdkN(activity); + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) + public void liveData_partiallyObscuredActivityWithFragment_minSdkN() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + CollectingSupportFragment fragment = new CollectingSupportFragment(); + mActivityTestRule.runOnUiThread(() -> activity.replaceFragment(fragment)); + + liveData_partiallyObscuredLifecycleOwner_minSdkN(fragment); + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) + public void liveData_partiallyObscuredActivityFragmentInFragment_minSdkN() throws Throwable { + CollectingSupportActivity activity = mActivityTestRule.getActivity(); + CollectingSupportFragment fragment = new CollectingSupportFragment(); + CollectingSupportFragment fragment2 = new CollectingSupportFragment(); + mActivityTestRule.runOnUiThread(() -> { + activity.replaceFragment(fragment); + fragment.replaceFragment(fragment2); + }); + + liveData_partiallyObscuredLifecycleOwner_minSdkN(fragment2); + } + + private void liveData_partiallyObscuredLifecycleOwner_maxSdkM(LifecycleOwner lifecycleOwner) + throws Throwable { + final AtomicInteger atomicInteger = new AtomicInteger(0); + MutableLiveData<Integer> mutableLiveData = new MutableLiveData<>(); + mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(0)); + + TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule); + + mutableLiveData.observe(lifecycleOwner, atomicInteger::set); + + final FragmentActivity dialogActivity = launchDialog(); + + TestUtils.waitTillCreated(lifecycleOwner, mActivityTestRule); + + // Change the LiveData value and assert that the observer is not called given that the + // lifecycle is in the CREATED state. + mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(1)); + assertThat(atomicInteger.get(), is(0)); + + // Finish the dialog Activity, wait for the main activity to be resumed, and assert that + // the observer's onChanged method is called. + mActivityTestRule.runOnUiThread(dialogActivity::finish); + TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule); + assertThat(atomicInteger.get(), is(1)); + } + + private void liveData_partiallyObscuredLifecycleOwner_minSdkN(LifecycleOwner lifecycleOwner) + throws Throwable { + final AtomicInteger atomicInteger = new AtomicInteger(0); + MutableLiveData<Integer> mutableLiveData = new MutableLiveData<>(); + mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(0)); + + TestUtils.waitTillResumed(lifecycleOwner, mActivityTestRule); + + mutableLiveData.observe(lifecycleOwner, atomicInteger::set); + + // Launch the NavigationDialogActivity, partially obscuring the activity, and wait for the + // lifecycleOwner to hit onPause (or enter the STARTED state). On API 24 and above, this + // onPause should be the last lifecycle method called (and the STARTED state should be the + // final resting state). + launchDialog(); + TestUtils.waitTillStarted(lifecycleOwner, mActivityTestRule); + + // Change the LiveData's value and verify that the observer's onChanged method is called + // since we are in the STARTED state. + mActivityTestRule.runOnUiThread(() -> mutableLiveData.setValue(1)); + assertThat(atomicInteger.get(), is(1)); + } + + private FragmentActivity launchDialog() throws Throwable { + Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor( + NavigationDialogActivity.class.getCanonicalName(), null, false); + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + instrumentation.addMonitor(monitor); + + FragmentActivity activity = mActivityTestRule.getActivity(); + // helps with less flaky API 16 tests + Intent intent = new Intent(activity, NavigationDialogActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + activity.startActivity(intent); + FragmentActivity fragmentActivity = (FragmentActivity) monitor.waitForActivity(); + TestUtils.waitTillResumed(fragmentActivity, mActivityTestRule); + return fragmentActivity; + } +} diff --git a/android/arch/lifecycle/LiveDataTest.java b/android/arch/lifecycle/LiveDataTest.java index 9f0b4257..647d5d7a 100644 --- a/android/arch/lifecycle/LiveDataTest.java +++ b/android/arch/lifecycle/LiveDataTest.java @@ -53,18 +53,29 @@ import org.mockito.Mockito; public class LiveDataTest { private PublicLiveData<String> mLiveData; private LifecycleOwner mOwner; + private LifecycleOwner mOwner2; private LifecycleRegistry mRegistry; + private LifecycleRegistry mRegistry2; private MethodExec mActiveObserversChanged; private boolean mInObserver; @Before public void init() { mLiveData = new PublicLiveData<>(); + + mActiveObserversChanged = mock(MethodExec.class); + mLiveData.activeObserversChanged = mActiveObserversChanged; + mOwner = mock(LifecycleOwner.class); + mRegistry = new LifecycleRegistry(mOwner); when(mOwner.getLifecycle()).thenReturn(mRegistry); - mActiveObserversChanged = mock(MethodExec.class); - mLiveData.activeObserversChanged = mActiveObserversChanged; + + mOwner2 = mock(LifecycleOwner.class); + + mRegistry2 = new LifecycleRegistry(mOwner2); + when(mOwner2.getLifecycle()).thenReturn(mRegistry2); + mInObserver = false; } @@ -159,14 +170,11 @@ public class LiveDataTest { @Test public void testAddSameObserverIn2LifecycleOwners() { Observer<String> observer = (Observer<String>) mock(Observer.class); - LifecycleOwner owner2 = mock(LifecycleOwner.class); - LifecycleRegistry registry2 = new LifecycleRegistry(owner2); - when(owner2.getLifecycle()).thenReturn(registry2); mLiveData.observe(mOwner, observer); Throwable throwable = null; try { - mLiveData.observe(owner2, observer); + mLiveData.observe(mOwner2, observer); } catch (Throwable t) { throwable = t; } @@ -456,6 +464,210 @@ public class LiveDataTest { inOrder.verifyNoMoreInteractions(); } + @Test + public void setValue_neverActive_observerOnChangedNotCalled() { + Observer<String> observer = (Observer<String>) mock(Observer.class); + mLiveData.observe(mOwner, observer); + + mLiveData.setValue("1"); + + verify(observer, never()).onChanged(anyString()); + } + + @Test + public void setValue_twoObserversTwoStartedOwners_onChangedCalledOnBoth() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + + mLiveData.setValue("1"); + + verify(observer1).onChanged("1"); + verify(observer2).onChanged("1"); + } + + @Test + public void setValue_twoObserversOneStartedOwner_onChangedCalledOnOneCorrectObserver() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + mLiveData.setValue("1"); + + verify(observer1).onChanged("1"); + verify(observer2, never()).onChanged(anyString()); + } + + @Test + public void setValue_twoObserversBothStartedAfterSetValue_onChangedCalledOnBoth() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mLiveData.setValue("1"); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(observer1).onChanged("1"); + verify(observer1).onChanged("1"); + } + + @Test + public void setValue_twoObserversOneStartedAfterSetValue_onChangedCalledOnCorrectObserver() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mLiveData.setValue("1"); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(observer1).onChanged("1"); + verify(observer2, never()).onChanged(anyString()); + } + + @Test + public void setValue_twoObserversOneStarted_liveDataBecomesActive() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(mActiveObserversChanged).onCall(true); + } + + @Test + public void setValue_twoObserversOneStopped_liveDataStaysActive() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + + verify(mActiveObserversChanged).onCall(true); + + reset(mActiveObserversChanged); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + } + + /** + * Verifies that if a lifecycle's state changes without an event, and changes to something that + * LiveData would become inactive in response to, LiveData will detect the change upon new data + * being set and become inactive. Also verifies that once the lifecycle enters into a state + * that LiveData should become active to, that it does indeed become active. + */ + @Test + public void liveDataActiveStateIsManagedCorrectlyWithoutEvent_oneObserver() { + Observer<String> observer = (Observer<String>) mock(Observer.class); + mLiveData.observe(mOwner, observer); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + + // Marking state as CREATED should call onInactive. + reset(mActiveObserversChanged); + mRegistry.markState(Lifecycle.State.CREATED); + verify(mActiveObserversChanged).onCall(false); + reset(mActiveObserversChanged); + + // Setting a new value should trigger LiveData to realize the Lifecycle it is observing + // is in a state where the LiveData should be inactive, so the LiveData will call onInactive + // and the Observer shouldn't be affected. + mLiveData.setValue("1"); + + // state is already CREATED so should not call again + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + verify(observer, never()).onChanged(anyString()); + + // Sanity check. Because we've only marked the state as CREATED, sending ON_START + // should re-dispatch events. + reset(mActiveObserversChanged); + reset(observer); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + verify(mActiveObserversChanged).onCall(true); + verify(observer).onChanged("1"); + } + + /** + * This test verifies that LiveData will detect changes in LifecycleState that would make it + * inactive upon the setting of new data, but only if all of the Lifecycles it's observing + * are all in those states. It also makes sure that once it is inactive, that it will become + * active again once one of the lifecycles it's observing moves to an appropriate state. + */ + @Test + public void liveDataActiveStateIsManagedCorrectlyWithoutEvent_twoObservers() { + Observer<String> observer1 = mock(Observer.class); + Observer<String> observer2 = mock(Observer.class); + + mLiveData.observe(mOwner, observer1); + mLiveData.observe(mOwner2, observer2); + + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mRegistry2.handleLifecycleEvent(Lifecycle.Event.ON_START); + + // Marking the state to created won't change LiveData to be inactive. + reset(mActiveObserversChanged); + mRegistry.markState(Lifecycle.State.CREATED); + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + + // After setting a value, the LiveData will stay active because there is still a STARTED + // lifecycle being observed. The one Observer associated with the STARTED lifecycle will + // also have been called, but the other Observer will not have been called. + reset(observer1); + reset(observer2); + mLiveData.setValue("1"); + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + verify(observer1, never()).onChanged(anyString()); + verify(observer2).onChanged("1"); + + // Now we set the other Lifecycle to be inactive, live data should become inactive. + reset(observer1); + reset(observer2); + mRegistry2.markState(Lifecycle.State.CREATED); + verify(mActiveObserversChanged).onCall(false); + verify(observer1, never()).onChanged(anyString()); + verify(observer2, never()).onChanged(anyString()); + + // Now we post another value, because both lifecycles are in the Created state, live data + // will not dispatch any values + reset(mActiveObserversChanged); + mLiveData.setValue("2"); + verify(mActiveObserversChanged, never()).onCall(anyBoolean()); + verify(observer1, never()).onChanged(anyString()); + verify(observer2, never()).onChanged(anyString()); + + // Now that the first Lifecycle has been moved back to the Resumed state, the LiveData will + // be made active and it's associated Observer will be called with the new value, but the + // Observer associated with the Lifecycle that is still in the Created state won't be + // called. + reset(mActiveObserversChanged); + mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + verify(mActiveObserversChanged).onCall(true); + verify(observer1).onChanged("2"); + verify(observer2, never()).onChanged(anyString()); + } + @SuppressWarnings("WeakerAccess") static class PublicLiveData<T> extends LiveData<T> { // cannot spy due to internal calls diff --git a/android/arch/lifecycle/MediatorLiveData.java b/android/arch/lifecycle/MediatorLiveData.java index 672b3a3b..58647394 100644 --- a/android/arch/lifecycle/MediatorLiveData.java +++ b/android/arch/lifecycle/MediatorLiveData.java @@ -19,16 +19,49 @@ package android.arch.lifecycle; import android.arch.core.internal.SafeIterableMap; import android.support.annotation.CallSuper; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.Map; /** - * {@link LiveData} subclass which may observer other {@code LiveData} objects and react on + * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on * {@code OnChanged} events from them. * <p> * This class correctly propagates its active/inactive states down to source {@code LiveData} * objects. + * <p> + * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them + * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object: + * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for + * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback + * is called for either of them, we set a new value in {@code liveDataMerger}. + * + * <pre> + * LiveData<Integer> liveData1 = ...; + * LiveData<Integer> liveData2 = ...; + * + * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>(); + * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value)); + * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value)); + * </pre> + * <p> + * Let's consider that we only want 10 values emitted by {@code liveData1}, to be + * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code + * liveData1} and remove it as a source. + * <pre> + * liveDataMerger.addSource(liveData1, new Observer<Integer>() { + * private int count = 1; + * + * {@literal @}Override public void onChanged(@Nullable Integer s) { + * count++; + * liveDataMerger.setValue(s); + * if (count > 10) { + * liveDataMerger.removeSource(liveData1); + * } + * } + * }); + * </pre> * * @param <T> The type of data hold by this instance */ @@ -49,7 +82,7 @@ public class MediatorLiveData<T> extends MutableLiveData<T> { * @param <S> The type of data hold by {@code source} LiveData */ @MainThread - public <S> void addSource(LiveData<S> source, Observer<S> onChanged) { + public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<S> onChanged) { Source<S> e = new Source<>(source, onChanged); Source<?> existing = mSources.putIfAbsent(source, e); if (existing != null && existing.mObserver != onChanged) { @@ -71,7 +104,7 @@ public class MediatorLiveData<T> extends MutableLiveData<T> { * @param <S> the type of data hold by {@code source} LiveData */ @MainThread - public <S> void removeSource(LiveData<S> toRemote) { + public <S> void removeSource(@NonNull LiveData<S> toRemote) { Source<?> source = mSources.remove(toRemote); if (source != null) { source.unplug(); diff --git a/android/arch/lifecycle/MissingClassTest.java b/android/arch/lifecycle/MissingClassTest.java new file mode 100644 index 00000000..81a07564 --- /dev/null +++ b/android/arch/lifecycle/MissingClassTest.java @@ -0,0 +1,44 @@ +/* + * 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 android.arch.lifecycle; + +import android.app.PictureInPictureParams; +import android.os.Build; +import android.support.test.filters.SdkSuppress; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1) +@SmallTest +public class MissingClassTest { + public static class ObserverWithMissingClasses { + @SuppressWarnings("unused") + public void newApiMethod(PictureInPictureParams params) {} + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() {} + } + + @Test(expected = IllegalArgumentException.class) + public void testMissingApi() { + new ReflectiveGenericLifecycleObserver(new ObserverWithMissingClasses()); + } +} diff --git a/android/arch/lifecycle/PartiallyCoveredActivityTest.java b/android/arch/lifecycle/PartiallyCoveredActivityTest.java new file mode 100644 index 00000000..07a9dc5a --- /dev/null +++ b/android/arch/lifecycle/PartiallyCoveredActivityTest.java @@ -0,0 +1,200 @@ +/* + * 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 android.arch.lifecycle; + +import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME; +import static android.arch.lifecycle.Lifecycle.Event.ON_START; +import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; +import static android.arch.lifecycle.TestUtils.OrderedTuples.CREATE; +import static android.arch.lifecycle.TestUtils.OrderedTuples.DESTROY; +import static android.arch.lifecycle.TestUtils.OrderedTuples.PAUSE; +import static android.arch.lifecycle.TestUtils.OrderedTuples.RESUME; +import static android.arch.lifecycle.TestUtils.OrderedTuples.START; +import static android.arch.lifecycle.TestUtils.OrderedTuples.STOP; +import static android.arch.lifecycle.TestUtils.flatMap; +import static android.arch.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT; +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +import android.app.Instrumentation; +import android.arch.lifecycle.testapp.CollectingLifecycleOwner; +import android.arch.lifecycle.testapp.CollectingSupportActivity; +import android.arch.lifecycle.testapp.CollectingSupportFragment; +import android.arch.lifecycle.testapp.NavigationDialogActivity; +import android.arch.lifecycle.testapp.TestEvent; +import android.content.Intent; +import android.os.Build; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; +import android.support.v4.app.FragmentActivity; +import android.support.v4.util.Pair; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * Runs tests about the state when an activity is partially covered by another activity. Pre + * API 24, framework behavior changes so the test rely on whether state is saved or not and makes + * assertions accordingly. + */ +@SuppressWarnings("unchecked") +@RunWith(Parameterized.class) +@LargeTest +public class PartiallyCoveredActivityTest { + private static final List[] IF_SAVED = new List[]{ + // when overlaid + flatMap(CREATE, START, RESUME, PAUSE, + singletonList(new Pair<>(LIFECYCLE_EVENT, ON_STOP))), + // post dialog dismiss + asList(new Pair<>(OWNER_CALLBACK, ON_RESUME), + new Pair<>(LIFECYCLE_EVENT, ON_START), + new Pair<>(LIFECYCLE_EVENT, ON_RESUME)), + // post finish + flatMap(PAUSE, STOP, DESTROY)}; + + private static final List[] IF_NOT_SAVED = new List[]{ + // when overlaid + flatMap(CREATE, START, RESUME, PAUSE), + // post dialog dismiss + flatMap(RESUME), + // post finish + flatMap(PAUSE, STOP, DESTROY)}; + + private static final boolean sShouldSave = Build.VERSION.SDK_INT < Build.VERSION_CODES.N; + private static final List<Pair<TestEvent, Lifecycle.Event>>[] EXPECTED = + sShouldSave ? IF_SAVED : IF_NOT_SAVED; + + @Rule + public ActivityTestRule<CollectingSupportActivity> activityRule = + new ActivityTestRule<CollectingSupportActivity>( + CollectingSupportActivity.class) { + @Override + protected Intent getActivityIntent() { + // helps with less flaky API 16 tests + Intent intent = new Intent(InstrumentationRegistry.getTargetContext(), + CollectingSupportActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return intent; + } + }; + private final boolean mDismissDialog; + + @Parameterized.Parameters(name = "dismissDialog_{0}") + public static List<Boolean> dismissDialog() { + return asList(true, false); + } + + public PartiallyCoveredActivityTest(boolean dismissDialog) { + mDismissDialog = dismissDialog; + } + + @Test + public void coveredWithDialog_activity() throws Throwable { + final CollectingSupportActivity activity = activityRule.getActivity(); + runTest(activity); + } + + @Test + public void coveredWithDialog_fragment() throws Throwable { + CollectingSupportFragment fragment = new CollectingSupportFragment(); + activityRule.runOnUiThread(() -> activityRule.getActivity().replaceFragment(fragment)); + runTest(fragment); + } + + @Test + public void coveredWithDialog_childFragment() throws Throwable { + CollectingSupportFragment parentFragment = new CollectingSupportFragment(); + CollectingSupportFragment childFragment = new CollectingSupportFragment(); + activityRule.runOnUiThread(() -> { + activityRule.getActivity().replaceFragment(parentFragment); + parentFragment.replaceFragment(childFragment); + }); + runTest(childFragment); + } + + private void runTest(CollectingLifecycleOwner owner) throws Throwable { + TestUtils.waitTillResumed(owner, activityRule); + FragmentActivity dialog = launchDialog(); + assertStateSaving(); + waitForIdle(); + assertThat(owner.copyCollectedEvents(), is(EXPECTED[0])); + List<Pair<TestEvent, Lifecycle.Event>> expected; + if (mDismissDialog) { + dialog.finish(); + TestUtils.waitTillResumed(activityRule.getActivity(), activityRule); + assertThat(owner.copyCollectedEvents(), is(flatMap(EXPECTED[0], EXPECTED[1]))); + expected = flatMap(EXPECTED[0], EXPECTED[1], EXPECTED[2]); + } else { + expected = flatMap(CREATE, START, RESUME, PAUSE, STOP, DESTROY); + } + CollectingSupportActivity activity = activityRule.getActivity(); + activityRule.finishActivity(); + TestUtils.waitTillDestroyed(activity, activityRule); + assertThat(owner.copyCollectedEvents(), is(expected)); + } + + // test sanity + private void assertStateSaving() throws ExecutionException, InterruptedException { + final CollectingSupportActivity activity = activityRule.getActivity(); + if (sShouldSave) { + // state should be saved. wait for it to be saved + assertThat("test sanity", + activity.waitForStateSave(20), is(true)); + assertThat("test sanity", activity.getSupportFragmentManager() + .isStateSaved(), is(true)); + } else { + // should should not be saved + assertThat("test sanity", activity.getSupportFragmentManager() + .isStateSaved(), is(false)); + } + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private FragmentActivity launchDialog() throws Throwable { + Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor( + NavigationDialogActivity.class.getCanonicalName(), null, false); + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + instrumentation.addMonitor(monitor); + + FragmentActivity activity = activityRule.getActivity(); + + Intent intent = new Intent(activity, NavigationDialogActivity.class); + // disabling animations helps with less flaky API 16 tests + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + activity.startActivity(intent); + FragmentActivity fragmentActivity = (FragmentActivity) monitor.waitForActivity(); + TestUtils.waitTillResumed(fragmentActivity, activityRule); + return fragmentActivity; + } +} diff --git a/android/arch/lifecycle/ProcessLifecycleOwner.java b/android/arch/lifecycle/ProcessLifecycleOwner.java index e2a12563..179e2c47 100644 --- a/android/arch/lifecycle/ProcessLifecycleOwner.java +++ b/android/arch/lifecycle/ProcessLifecycleOwner.java @@ -22,6 +22,7 @@ import android.arch.lifecycle.ReportFragment.ActivityInitializationListener; import android.content.Context; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; /** @@ -156,7 +157,8 @@ public class ProcessLifecycleOwner implements LifecycleOwner { app.registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - ReportFragment .get(activity).setProcessListener(mInitializationListener); + ReportFragment.injectIfNeededIn(activity); + ReportFragment.get(activity).setProcessListener(mInitializationListener); } @Override @@ -171,6 +173,7 @@ public class ProcessLifecycleOwner implements LifecycleOwner { }); } + @NonNull @Override public Lifecycle getLifecycle() { return mRegistry; diff --git a/android/arch/lifecycle/LifecycleRuntimeTrojanProvider.java b/android/arch/lifecycle/ProcessLifecycleOwnerInitializer.java index ac278c0c..8ba297fe 100644 --- a/android/arch/lifecycle/LifecycleRuntimeTrojanProvider.java +++ b/android/arch/lifecycle/ProcessLifecycleOwnerInitializer.java @@ -29,10 +29,9 @@ import android.support.annotation.RestrictTo; * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class LifecycleRuntimeTrojanProvider extends ContentProvider { +public class ProcessLifecycleOwnerInitializer extends ContentProvider { @Override public boolean onCreate() { - LifecycleDispatcher.init(getContext()); ProcessLifecycleOwner.init(getContext()); return true; } diff --git a/android/arch/lifecycle/ProcessOwnerTest.java b/android/arch/lifecycle/ProcessOwnerTest.java index 37bdcdb4..77baf94c 100644 --- a/android/arch/lifecycle/ProcessOwnerTest.java +++ b/android/arch/lifecycle/ProcessOwnerTest.java @@ -31,6 +31,7 @@ import android.arch.lifecycle.Lifecycle.Event; import android.arch.lifecycle.testapp.NavigationDialogActivity; import android.arch.lifecycle.testapp.NavigationTestActivityFirst; import android.arch.lifecycle.testapp.NavigationTestActivitySecond; +import android.arch.lifecycle.testapp.NonSupportActivity; import android.content.Context; import android.content.Intent; import android.support.test.InstrumentationRegistry; @@ -95,6 +96,22 @@ public class ProcessOwnerTest { } @Test + public void testNavigationToNonSupport() throws Throwable { + FragmentActivity firstActivity = setupObserverOnResume(); + Instrumentation.ActivityMonitor monitor = new Instrumentation.ActivityMonitor( + NonSupportActivity.class.getCanonicalName(), null, false); + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + instrumentation.addMonitor(monitor); + + Intent intent = new Intent(firstActivity, NonSupportActivity.class); + firstActivity.finish(); + firstActivity.startActivity(intent); + NonSupportActivity secondActivity = (NonSupportActivity) monitor.waitForActivity(); + assertThat("Failed to navigate", secondActivity, notNullValue()); + checkProcessObserverSilent(secondActivity); + } + + @Test public void testRecreation() throws Throwable { FragmentActivity activity = setupObserverOnResume(); FragmentActivity recreated = TestUtils.recreateActivity(activity, activityTestRule); @@ -164,4 +181,11 @@ public class ProcessOwnerTest { activityTestRule.runOnUiThread(() -> ProcessLifecycleOwner.get().getLifecycle().removeObserver(mObserver)); } + + private void checkProcessObserverSilent(NonSupportActivity activity) throws Throwable { + assertThat(activity.awaitResumedState(), is(true)); + assertThat(mObserver.mChangedState, is(false)); + activityTestRule.runOnUiThread(() -> + ProcessLifecycleOwner.get().getLifecycle().removeObserver(mObserver)); + } } diff --git a/android/arch/lifecycle/ReportFragment.java b/android/arch/lifecycle/ReportFragment.java index 3e4ece82..16a89ce8 100644 --- a/android/arch/lifecycle/ReportFragment.java +++ b/android/arch/lifecycle/ReportFragment.java @@ -28,7 +28,6 @@ import android.support.annotation.RestrictTo; */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class ReportFragment extends Fragment { - private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle" + ".LifecycleDispatcher.report_fragment_tag"; diff --git a/android/arch/lifecycle/TestUtils.java b/android/arch/lifecycle/TestUtils.java index f0214bfb..f7f9bbe5 100644 --- a/android/arch/lifecycle/TestUtils.java +++ b/android/arch/lifecycle/TestUtils.java @@ -16,16 +16,35 @@ package android.arch.lifecycle; +import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE; +import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY; +import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE; +import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME; +import static android.arch.lifecycle.Lifecycle.Event.ON_START; +import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; +import static android.arch.lifecycle.Lifecycle.State.CREATED; +import static android.arch.lifecycle.Lifecycle.State.DESTROYED; import static android.arch.lifecycle.Lifecycle.State.RESUMED; +import static android.arch.lifecycle.Lifecycle.State.STARTED; +import static android.arch.lifecycle.testapp.TestEvent.LIFECYCLE_EVENT; +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; import android.app.Activity; import android.app.Instrumentation; import android.app.Instrumentation.ActivityMonitor; +import android.arch.lifecycle.testapp.TestEvent; import android.support.test.InstrumentationRegistry; import android.support.test.rule.ActivityTestRule; -import android.support.v4.app.FragmentActivity; +import android.support.v4.util.Pair; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; class TestUtils { @@ -61,23 +80,88 @@ class TestUtils { return result; } - static void waitTillResumed(final FragmentActivity a, ActivityTestRule<?> activityRule) + static void waitTillCreated(final LifecycleOwner owner, ActivityTestRule<?> activityRule) + throws Throwable { + waitTillState(owner, activityRule, CREATED); + } + + static void waitTillStarted(final LifecycleOwner owner, ActivityTestRule<?> activityRule) + throws Throwable { + waitTillState(owner, activityRule, STARTED); + } + + static void waitTillResumed(final LifecycleOwner owner, ActivityTestRule<?> activityRule) + throws Throwable { + waitTillState(owner, activityRule, RESUMED); + } + + static void waitTillDestroyed(final LifecycleOwner owner, ActivityTestRule<?> activityRule) + throws Throwable { + waitTillState(owner, activityRule, DESTROYED); + } + + static void waitTillState(final LifecycleOwner owner, ActivityTestRule<?> activityRule, + Lifecycle.State state) throws Throwable { final CountDownLatch latch = new CountDownLatch(1); activityRule.runOnUiThread(() -> { - Lifecycle.State currentState = a.getLifecycle().getCurrentState(); - if (currentState == RESUMED) { + Lifecycle.State currentState = owner.getLifecycle().getCurrentState(); + if (currentState == state) { latch.countDown(); + } else { + owner.getLifecycle().addObserver(new LifecycleObserver() { + @OnLifecycleEvent(Lifecycle.Event.ON_ANY) + public void onStateChanged(LifecycleOwner provider) { + if (provider.getLifecycle().getCurrentState() == state) { + latch.countDown(); + provider.getLifecycle().removeObserver(this); + } + } + }); } - a.getLifecycle().addObserver(new LifecycleObserver() { - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - public void onStateChanged(LifecycleOwner provider) { - latch.countDown(); - provider.getLifecycle().removeObserver(this); - } - }); }); - latch.await(); + boolean latchResult = latch.await(1, TimeUnit.MINUTES); + assertThat("expected " + state + " never happened. Current state:" + + owner.getLifecycle().getCurrentState(), latchResult, is(true)); + + // wait for another loop to ensure all observers are called + activityRule.runOnUiThread(() -> { + // do nothing + }); } + @SafeVarargs + static <T> List<T> flatMap(List<T>... items) { + ArrayList<T> result = new ArrayList<>(); + for (List<T> item : items) { + result.addAll(item); + } + return result; + } + + /** + * Event tuples of {@link TestEvent} and {@link Lifecycle.Event} + * in the order they should arrive. + */ + @SuppressWarnings("unchecked") + static class OrderedTuples { + static final List<Pair<TestEvent, Lifecycle.Event>> CREATE = + Arrays.asList(new Pair(OWNER_CALLBACK, ON_CREATE), + new Pair(LIFECYCLE_EVENT, ON_CREATE)); + static final List<Pair<TestEvent, Lifecycle.Event>> START = + Arrays.asList(new Pair(OWNER_CALLBACK, ON_START), + new Pair(LIFECYCLE_EVENT, ON_START)); + static final List<Pair<TestEvent, Lifecycle.Event>> RESUME = + Arrays.asList(new Pair(OWNER_CALLBACK, ON_RESUME), + new Pair(LIFECYCLE_EVENT, ON_RESUME)); + static final List<Pair<TestEvent, Lifecycle.Event>> PAUSE = + Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_PAUSE), + new Pair(OWNER_CALLBACK, ON_PAUSE)); + static final List<Pair<TestEvent, Lifecycle.Event>> STOP = + Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_STOP), + new Pair(OWNER_CALLBACK, ON_STOP)); + static final List<Pair<TestEvent, Lifecycle.Event>> DESTROY = + Arrays.asList(new Pair(LIFECYCLE_EVENT, ON_DESTROY), + new Pair(OWNER_CALLBACK, ON_DESTROY)); + } } diff --git a/android/arch/lifecycle/Transformations.java b/android/arch/lifecycle/Transformations.java index 9ce9cbb7..c735f8ba 100644 --- a/android/arch/lifecycle/Transformations.java +++ b/android/arch/lifecycle/Transformations.java @@ -18,6 +18,7 @@ package android.arch.lifecycle; import android.arch.core.util.Function; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; /** @@ -60,7 +61,8 @@ public class Transformations { * @return a LiveData which emits resulting values */ @MainThread - public static <X, Y> LiveData<Y> map(LiveData<X> source, final Function<X, Y> func) { + public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source, + @NonNull final Function<X, Y> func) { final MediatorLiveData<Y> result = new MediatorLiveData<>(); result.addSource(source, new Observer<X>() { @Override @@ -120,8 +122,8 @@ public class Transformations { * @param <Y> a type of resulting LiveData */ @MainThread - public static <X, Y> LiveData<Y> switchMap(LiveData<X> trigger, - final Function<X, LiveData<Y>> func) { + public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger, + @NonNull final Function<X, LiveData<Y>> func) { final MediatorLiveData<Y> result = new MediatorLiveData<>(); result.addSource(trigger, new Observer<X>() { LiveData<Y> mSource; diff --git a/android/arch/lifecycle/ViewModelProvider.java b/android/arch/lifecycle/ViewModelProvider.java index 7ef591f3..29cbab8e 100644 --- a/android/arch/lifecycle/ViewModelProvider.java +++ b/android/arch/lifecycle/ViewModelProvider.java @@ -43,7 +43,8 @@ public class ViewModelProvider { * @param <T> The type parameter for the ViewModel. * @return a newly created ViewModel */ - <T extends ViewModel> T create(Class<T> modelClass); + @NonNull + <T extends ViewModel> T create(@NonNull Class<T> modelClass); } private final Factory mFactory; @@ -70,7 +71,7 @@ public class ViewModelProvider { * @param factory factory a {@code Factory} which will be used to instantiate * new {@code ViewModels} */ - public ViewModelProvider(ViewModelStore store, Factory factory) { + public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) { mFactory = factory; this.mViewModelStore = store; } @@ -88,7 +89,8 @@ public class ViewModelProvider { * @param <T> The type parameter for the ViewModel. * @return A ViewModel that is an instance of the given type {@code T}. */ - public <T extends ViewModel> T get(Class<T> modelClass) { + @NonNull + public <T extends ViewModel> T get(@NonNull Class<T> modelClass) { String canonicalName = modelClass.getCanonicalName(); if (canonicalName == null) { throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels"); @@ -136,8 +138,9 @@ public class ViewModelProvider { */ public static class NewInstanceFactory implements Factory { + @NonNull @Override - public <T extends ViewModel> T create(Class<T> modelClass) { + public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { //noinspection TryWithIdenticalCatches try { return modelClass.newInstance(); diff --git a/android/arch/lifecycle/ViewModelProviders.java b/android/arch/lifecycle/ViewModelProviders.java index 746162a9..b4b20aa4 100644 --- a/android/arch/lifecycle/ViewModelProviders.java +++ b/android/arch/lifecycle/ViewModelProviders.java @@ -139,8 +139,9 @@ public class ViewModelProviders { mApplication = application; } + @NonNull @Override - public <T extends ViewModel> T create(Class<T> modelClass) { + public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { if (AndroidViewModel.class.isAssignableFrom(modelClass)) { //noinspection TryWithIdenticalCatches try { diff --git a/android/arch/lifecycle/ViewModelStoreOwner.java b/android/arch/lifecycle/ViewModelStoreOwner.java index 50583056..e26fa325 100644 --- a/android/arch/lifecycle/ViewModelStoreOwner.java +++ b/android/arch/lifecycle/ViewModelStoreOwner.java @@ -16,6 +16,8 @@ package android.arch.lifecycle; +import android.support.annotation.NonNull; + /** * A scope that owns {@link ViewModelStore}. * <p> @@ -30,5 +32,6 @@ public interface ViewModelStoreOwner { * * @return a {@code ViewModelStore} */ + @NonNull ViewModelStore getViewModelStore(); } diff --git a/android/arch/lifecycle/ViewModelStores.java b/android/arch/lifecycle/ViewModelStores.java index 8c17dd98..d7d769d6 100644 --- a/android/arch/lifecycle/ViewModelStores.java +++ b/android/arch/lifecycle/ViewModelStores.java @@ -19,6 +19,7 @@ package android.arch.lifecycle; import static android.arch.lifecycle.HolderFragment.holderFragmentFor; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; @@ -38,7 +39,7 @@ public class ViewModelStores { * @return a {@code ViewModelStore} */ @MainThread - public static ViewModelStore of(FragmentActivity activity) { + public static ViewModelStore of(@NonNull FragmentActivity activity) { return holderFragmentFor(activity).getViewModelStore(); } @@ -49,7 +50,7 @@ public class ViewModelStores { * @return a {@code ViewModelStore} */ @MainThread - public static ViewModelStore of(Fragment fragment) { + public static ViewModelStore of(@NonNull Fragment fragment) { return holderFragmentFor(fragment).getViewModelStore(); } } diff --git a/android/arch/lifecycle/testapp/CollectingActivity.java b/android/arch/lifecycle/testapp/CollectingLifecycleOwner.java index 6e243b6c..4213cab9 100644 --- a/android/arch/lifecycle/testapp/CollectingActivity.java +++ b/android/arch/lifecycle/testapp/CollectingLifecycleOwner.java @@ -17,21 +17,20 @@ package android.arch.lifecycle.testapp; import android.arch.lifecycle.Lifecycle; -import android.util.Pair; +import android.arch.lifecycle.LifecycleOwner; +import android.support.v4.util.Pair; import java.util.List; /** * For activities that collect their events. */ -public interface CollectingActivity { - long TIMEOUT = 5; - +public interface CollectingLifecycleOwner extends LifecycleOwner { /** - * Return collected events + * Return a copy of currently collected events * * @return The list of collected events. * @throws InterruptedException */ - List<Pair<TestEvent, Lifecycle.Event>> waitForCollectedEvents() throws InterruptedException; + List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents(); } diff --git a/android/arch/lifecycle/testapp/CollectingSupportActivity.java b/android/arch/lifecycle/testapp/CollectingSupportActivity.java new file mode 100644 index 00000000..f38d4224 --- /dev/null +++ b/android/arch/lifecycle/testapp/CollectingSupportActivity.java @@ -0,0 +1,113 @@ +/* + * 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 android.arch.lifecycle.testapp; + +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; + +import android.arch.lifecycle.Lifecycle.Event; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.util.Pair; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * LifecycleRegistryOwner that extends FragmentActivity. + */ +public class CollectingSupportActivity extends FragmentActivity implements + CollectingLifecycleOwner { + + private final List<Pair<TestEvent, Event>> mCollectedEvents = new ArrayList<>(); + private TestObserver mTestObserver = new TestObserver(mCollectedEvents); + private CountDownLatch mSavedStateLatch = new CountDownLatch(1); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FrameLayout layout = new FrameLayout(this); + layout.setId(R.id.fragment_container); + setContentView(layout); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_CREATE)); + getLifecycle().addObserver(mTestObserver); + } + + /** + * replaces the main content fragment w/ the given fragment. + */ + public void replaceFragment(Fragment fragment) { + getSupportFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, fragment) + .commitNow(); + } + + @Override + protected void onStart() { + super.onStart(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_START)); + } + + @Override + protected void onResume() { + super.onResume(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_RESUME)); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_DESTROY)); + } + + @Override + protected void onStop() { + super.onStop(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_STOP)); + } + + @Override + protected void onPause() { + super.onPause(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Event.ON_PAUSE)); + // helps with less flaky API 16 tests. + overridePendingTransition(0, 0); + } + + @Override + public List<Pair<TestEvent, Event>> copyCollectedEvents() { + return new ArrayList<>(mCollectedEvents); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mSavedStateLatch.countDown(); + } + + /** + * Waits for onSaveInstanceState to be called. + */ + public boolean waitForStateSave(@SuppressWarnings("SameParameterValue") int seconds) + throws InterruptedException { + return mSavedStateLatch.await(seconds, TimeUnit.SECONDS); + } +} diff --git a/android/arch/lifecycle/testapp/CollectingSupportFragment.java b/android/arch/lifecycle/testapp/CollectingSupportFragment.java new file mode 100644 index 00000000..9bbbe165 --- /dev/null +++ b/android/arch/lifecycle/testapp/CollectingSupportFragment.java @@ -0,0 +1,104 @@ +/* + * 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 android.arch.lifecycle.testapp; + +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; + +import android.annotation.SuppressLint; +import android.arch.lifecycle.Lifecycle; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import java.util.ArrayList; +import java.util.List; + +/** + * A support fragment that collects all of its events. + */ +@SuppressLint("ValidFragment") +public class CollectingSupportFragment extends Fragment implements CollectingLifecycleOwner { + private final List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents = + new ArrayList<>(); + private TestObserver mTestObserver = new TestObserver(mCollectedEvents); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_CREATE)); + getLifecycle().addObserver(mTestObserver); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + //noinspection ConstantConditions + FrameLayout layout = new FrameLayout(container.getContext()); + layout.setId(R.id.child_fragment_container); + return layout; + } + + /** + * Runs a replace fragment transaction with 'fragment' on this Fragment. + */ + public void replaceFragment(Fragment fragment) { + getChildFragmentManager() + .beginTransaction() + .add(R.id.child_fragment_container, fragment) + .commitNow(); + } + + @Override + public void onStart() { + super.onStart(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_START)); + } + + @Override + public void onResume() { + super.onResume(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_RESUME)); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_DESTROY)); + } + + @Override + public void onStop() { + super.onStop(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_STOP)); + } + + @Override + public void onPause() { + super.onPause(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_PAUSE)); + } + + @Override + public List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents() { + return new ArrayList<>(mCollectedEvents); + } +} diff --git a/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java b/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java index d8f4fb39..cdf577c1 100644 --- a/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java +++ b/android/arch/lifecycle/testapp/FrameworkLifecycleRegistryActivity.java @@ -16,27 +16,29 @@ package android.arch.lifecycle.testapp; -import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; +import static android.arch.lifecycle.testapp.TestEvent.OWNER_CALLBACK; import android.app.Activity; import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleRegistry; import android.arch.lifecycle.LifecycleRegistryOwner; import android.os.Bundle; -import android.util.Pair; +import android.support.annotation.NonNull; +import android.support.v4.util.Pair; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; /** * LifecycleRegistryOwner that extends framework activity. */ +@SuppressWarnings("deprecation") public class FrameworkLifecycleRegistryActivity extends Activity implements - LifecycleRegistryOwner, CollectingActivity { + LifecycleRegistryOwner, CollectingLifecycleOwner { private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); + @NonNull @Override public LifecycleRegistry getLifecycle() { return mLifecycleRegistry; @@ -49,49 +51,43 @@ public class FrameworkLifecycleRegistryActivity extends Activity implements @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_CREATE)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_CREATE)); getLifecycle().addObserver(mTestObserver); } @Override protected void onStart() { super.onStart(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_START)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_START)); } @Override protected void onResume() { super.onResume(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_RESUME)); - finish(); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_RESUME)); } @Override protected void onDestroy() { super.onDestroy(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_DESTROY)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_DESTROY)); mLatch.countDown(); } @Override protected void onStop() { super.onStop(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_STOP)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_STOP)); } @Override protected void onPause() { super.onPause(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_PAUSE)); + mCollectedEvents.add(new Pair<>(OWNER_CALLBACK, Lifecycle.Event.ON_PAUSE)); } - /** - * awaits for all events and returns them. - */ @Override - public List<Pair<TestEvent, Lifecycle.Event>> waitForCollectedEvents() - throws InterruptedException { - mLatch.await(TIMEOUT, TimeUnit.SECONDS); - return mCollectedEvents; + public List<Pair<TestEvent, Lifecycle.Event>> copyCollectedEvents() { + return new ArrayList<>(mCollectedEvents); } } diff --git a/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java b/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java deleted file mode 100644 index 5f33c282..00000000 --- a/android/arch/lifecycle/testapp/FullLifecycleTestActivity.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.lifecycle.testapp; - -import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; - -import android.arch.lifecycle.Lifecycle; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.util.Pair; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * Activity for testing full lifecycle - */ -public class FullLifecycleTestActivity extends FragmentActivity implements CollectingActivity { - - private List<Pair<TestEvent, Lifecycle.Event>> mCollectedEvents = new ArrayList<>(); - private TestObserver mTestObserver = new TestObserver(mCollectedEvents); - private CountDownLatch mLatch = new CountDownLatch(1); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_CREATE)); - getLifecycle().addObserver(mTestObserver); - } - - @Override - protected void onStart() { - super.onStart(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_START)); - } - - @Override - protected void onResume() { - super.onResume(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_RESUME)); - finish(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_DESTROY)); - mLatch.countDown(); - } - - @Override - protected void onStop() { - super.onStop(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_STOP)); - } - - @Override - protected void onPause() { - super.onPause(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Lifecycle.Event.ON_PAUSE)); - } - - /** - * awaits for all events and returns them. - */ - @Override - public List<Pair<TestEvent, Lifecycle.Event>> waitForCollectedEvents() - throws InterruptedException { - mLatch.await(TIMEOUT, TimeUnit.SECONDS); - return mCollectedEvents; - } -} diff --git a/android/arch/lifecycle/testapp/MainActivity.java b/android/arch/lifecycle/testapp/MainActivity.java deleted file mode 100644 index b9d59142..00000000 --- a/android/arch/lifecycle/testapp/MainActivity.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.arch.lifecycle.testapp; - -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; - -/** - * Simple test activity - */ -public class MainActivity extends FragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity); - } -} diff --git a/android/arch/lifecycle/testapp/NavigationDialogActivity.java b/android/arch/lifecycle/testapp/NavigationDialogActivity.java index 0ae94033..7d53528f 100644 --- a/android/arch/lifecycle/testapp/NavigationDialogActivity.java +++ b/android/arch/lifecycle/testapp/NavigationDialogActivity.java @@ -22,4 +22,10 @@ import android.support.v4.app.FragmentActivity; * an activity with Dialog theme. */ public class NavigationDialogActivity extends FragmentActivity { + @Override + protected void onPause() { + super.onPause(); + // helps with less flaky API 16 tests + overridePendingTransition(0, 0); + } } diff --git a/android/arch/lifecycle/testapp/NonSupportActivity.java b/android/arch/lifecycle/testapp/NonSupportActivity.java new file mode 100644 index 00000000..835d846a --- /dev/null +++ b/android/arch/lifecycle/testapp/NonSupportActivity.java @@ -0,0 +1,85 @@ +/* + * 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 android.arch.lifecycle.testapp; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Activity which doesn't extend FragmentActivity, to test ProcessLifecycleOwner because it + * should work anyway. + */ +public class NonSupportActivity extends Activity { + + private static final int TIMEOUT = 1; //secs + private final Lock mLock = new ReentrantLock(); + private Condition mIsResumedCondition = mLock.newCondition(); + private boolean mIsResumed = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onResume() { + super.onResume(); + mLock.lock(); + try { + mIsResumed = true; + mIsResumedCondition.signalAll(); + } finally { + mLock.unlock(); + } + } + + @Override + protected void onPause() { + super.onPause(); + mLock.lock(); + try { + mIsResumed = false; + } finally { + mLock.unlock(); + } + } + + /** + * awaits resumed state + * @return + * @throws InterruptedException + */ + public boolean awaitResumedState() throws InterruptedException { + mLock.lock(); + try { + while (!mIsResumed) { + if (!mIsResumedCondition.await(TIMEOUT, TimeUnit.SECONDS)) { + return false; + } + } + return true; + } finally { + mLock.unlock(); + } + } +} diff --git a/android/arch/lifecycle/testapp/SupportLifecycleRegistryActivity.java b/android/arch/lifecycle/testapp/SupportLifecycleRegistryActivity.java deleted file mode 100644 index c46c6d3e..00000000 --- a/android/arch/lifecycle/testapp/SupportLifecycleRegistryActivity.java +++ /dev/null @@ -1,95 +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 android.arch.lifecycle.testapp; - -import static android.arch.lifecycle.testapp.TestEvent.ACTIVITY_CALLBACK; - -import android.arch.lifecycle.Lifecycle.Event; -import android.arch.lifecycle.LifecycleRegistry; -import android.arch.lifecycle.LifecycleRegistryOwner; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.util.Pair; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * LifecycleRegistryOwner that extends FragmentActivity. - */ -public class SupportLifecycleRegistryActivity extends FragmentActivity implements - LifecycleRegistryOwner, CollectingActivity { - private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this); - @Override - public LifecycleRegistry getLifecycle() { - return mLifecycleRegistry; - } - - private List<Pair<TestEvent, Event>> mCollectedEvents = new ArrayList<>(); - private TestObserver mTestObserver = new TestObserver(mCollectedEvents); - private CountDownLatch mLatch = new CountDownLatch(1); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_CREATE)); - getLifecycle().addObserver(mTestObserver); - } - - @Override - protected void onStart() { - super.onStart(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_START)); - } - - @Override - protected void onResume() { - super.onResume(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_RESUME)); - finish(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_DESTROY)); - mLatch.countDown(); - } - - @Override - protected void onStop() { - super.onStop(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_STOP)); - } - - @Override - protected void onPause() { - super.onPause(); - mCollectedEvents.add(new Pair<>(ACTIVITY_CALLBACK, Event.ON_PAUSE)); - } - - /** - * awaits for all events and returns them. - */ - @Override - public List<Pair<TestEvent, Event>> waitForCollectedEvents() throws InterruptedException { - mLatch.await(TIMEOUT, TimeUnit.SECONDS); - return mCollectedEvents; - } -} diff --git a/android/arch/lifecycle/testapp/TestEvent.java b/android/arch/lifecycle/testapp/TestEvent.java index 0929f84a..788045a2 100644 --- a/android/arch/lifecycle/testapp/TestEvent.java +++ b/android/arch/lifecycle/testapp/TestEvent.java @@ -17,6 +17,6 @@ package android.arch.lifecycle.testapp; public enum TestEvent { - ACTIVITY_CALLBACK, - LIFECYCLE_EVENT + OWNER_CALLBACK, + LIFECYCLE_EVENT, } diff --git a/android/arch/lifecycle/testapp/TestObserver.java b/android/arch/lifecycle/testapp/TestObserver.java index c6112396..00b8e16d 100644 --- a/android/arch/lifecycle/testapp/TestObserver.java +++ b/android/arch/lifecycle/testapp/TestObserver.java @@ -28,7 +28,7 @@ import android.arch.lifecycle.Lifecycle.Event; import android.arch.lifecycle.LifecycleObserver; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.OnLifecycleEvent; -import android.util.Pair; +import android.support.v4.util.Pair; import java.util.List; |