LifecyleRegistry
This CL adds the helper class so that Fragments and Activities can
implement LifecycleProvider.
Test: com.android.support.lifecycle.LifecycleRegistryTest\
com.android.support.lifecycle.ObserverListTest
Bug: 32342385
Change-Id: Ie3402969b06760cb277cff0b176ffff2dd7d6e2a
diff --git a/lifecycle/common/src/main/java/com/android/support/lifecycle/Lifecycle.java b/lifecycle/common/src/main/java/com/android/support/lifecycle/Lifecycle.java
index 9ff11ef..7322bff 100644
--- a/lifecycle/common/src/main/java/com/android/support/lifecycle/Lifecycle.java
+++ b/lifecycle/common/src/main/java/com/android/support/lifecycle/Lifecycle.java
@@ -12,7 +12,6 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
- *
*/
package com.android.support.lifecycle;
@@ -22,12 +21,37 @@
@SuppressWarnings({"UnnecessaryInterfaceModifier", "WeakerAccess", "unused"})
public interface Lifecycle {
+ /**
+ * Adds a LifecycleObserver that will be notified when the owner LifecycleProvide changes state.
+ * <p>
+ * If this method is called while a state change is being dispatched, the given observer will
+ * not receive that event.
+ *
+ * @param observer The observer to notify.
+ */
@MainThread
- void addObserver(LifecycleObserver callback);
+ void addObserver(LifecycleObserver observer);
+
+ /**
+ * Removes the given observer from the observers list.
+ * <p>
+ * If this method is called while a state change is being dispatched,
+ * * If the given observer has not yet received that event, it will not receive it.
+ * * If the given observer has more than 1 methods that observes the currently dispatched event
+ * and at least one of them received the event, all of them will receive the event and the
+ * removal will happen afterwards.
+ *
+ * @param observer The observer to be removed.
+ */
@MainThread
- void removeObserver(LifecycleObserver removeCallback);
- @MainThread
- @State
+ void removeObserver(LifecycleObserver observer);
+
+ /**
+ * Returns the current state of the Lifecycle.
+ *
+ * @return The current state of the Lifecycle.
+ */
+ @MainThread @State
public int getCurrentState();
public static final int INITIALIZED = 1;
public static final int CREATED = INITIALIZED << 2;
@@ -39,7 +63,8 @@
public static final int FINISHED = DESTROYED << 2;
public static final int ANY = -1;
- @IntDef(value = {INITIALIZED, CREATED, STARTED, RESUMED, PAUSED, STOPPED, DESTROYED, ANY}, flag = true)
+ @IntDef(value = {INITIALIZED, CREATED, STARTED, RESUMED, PAUSED, STOPPED, DESTROYED, ANY},
+ flag = true)
public @interface State {
}
}
diff --git a/lifecycle/common/src/main/java/com/android/support/lifecycle/ReflectiveGenericLifecycleObserver.java b/lifecycle/common/src/main/java/com/android/support/lifecycle/ReflectiveGenericLifecycleObserver.java
index 8286981..5d1ea3a 100644
--- a/lifecycle/common/src/main/java/com/android/support/lifecycle/ReflectiveGenericLifecycleObserver.java
+++ b/lifecycle/common/src/main/java/com/android/support/lifecycle/ReflectiveGenericLifecycleObserver.java
@@ -46,6 +46,7 @@
invokeCallbacks(mInfo, source, state, previousState);
}
+ @SuppressWarnings("ConstantConditions")
private void invokeCallbacks(CallbackInfo info, LifecycleProvider source,
@Lifecycle.State int state, @Lifecycle.State int previousState) {
if ((info.mStates & state) != 0) {
@@ -54,7 +55,7 @@
invokeCallback(reference, source, previousState, state);
}
}
- // TODO prevent duplicate calls into the same method. Preferebly while parsing
+ // TODO prevent duplicate calls into the same method. Preferably while parsing
if (info.mSuper != null) {
invokeCallbacks(info.mSuper, source, state, previousState);
}
diff --git a/lifecycle/compiler/build.gradle b/lifecycle/compiler/build.gradle
index 808296f..335cdad 100644
--- a/lifecycle/compiler/build.gradle
+++ b/lifecycle/compiler/build.gradle
@@ -17,6 +17,7 @@
apply plugin: 'kotlin'
apply plugin: 'maven'
+apply plugin: 'checkstyle'
sourceSets {
test.java.srcDirs += 'src/tests/kotlin'
@@ -38,4 +39,13 @@
pom.artifactId = 'lifecycle-compiler'
}
}
+}
+
+task('checkstyle', type: Checkstyle) {
+ configFile rootProject.file('kotlin-style.xml')
+ source 'src/main/kotlin'
+ ignoreFailures false
+ showViolations true
+ include '**/*.kt'
+ classpath = files()
}
\ No newline at end of file
diff --git a/lifecycle/compiler/src/main/kotlin/com/android/support/lifecycle/LifecycleProcessor.kt b/lifecycle/compiler/src/main/kotlin/com/android/support/lifecycle/LifecycleProcessor.kt
index 8497dc0..fa6d0a6 100644
--- a/lifecycle/compiler/src/main/kotlin/com/android/support/lifecycle/LifecycleProcessor.kt
+++ b/lifecycle/compiler/src/main/kotlin/com/android/support/lifecycle/LifecycleProcessor.kt
@@ -12,7 +12,6 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
- *
*/
package com.android.support.lifecycle
@@ -264,4 +263,4 @@
data class StateMethod(val method: ExecutableElement, val onState: OnState)
data class LifecycleObserverInfo(val type: TypeElement, val methods: List<StateMethod>)
-}
\ No newline at end of file
+}
diff --git a/lifecycle/extensions/.gitignore b/lifecycle/extensions/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/lifecycle/extensions/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/lifecycle/extensions/build.gradle b/lifecycle/extensions/build.gradle
new file mode 100644
index 0000000..7b32435
--- /dev/null
+++ b/lifecycle/extensions/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 24
+ buildToolsVersion "25.0.0"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 24
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile project(":runtime")
+ testCompile 'junit:junit:4.12'
+}
diff --git a/lifecycle/extensions/proguard-rules.pro b/lifecycle/extensions/proguard-rules.pro
new file mode 100644
index 0000000..b7210d1
--- /dev/null
+++ b/lifecycle/extensions/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/yboyar/android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/lifecycle/extensions/src/main/AndroidManifest.xml b/lifecycle/extensions/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1ed5b75
--- /dev/null
+++ b/lifecycle/extensions/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.support.lifecycle.ext">
+</manifest>
diff --git a/lifecycle/kotlin-checkstyle.xml b/lifecycle/kotlin-checkstyle.xml
new file mode 100644
index 0000000..5cdad0a6
--- /dev/null
+++ b/lifecycle/kotlin-checkstyle.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
+<!-- this is a very limited set of checkstyle rules mainly because TreeWalker rules are not
+supported for kt files yet.-->
+<module name="Checker">
+ <property name="severity" value="warning"/>
+ <property name="charset" value="UTF-8"/>
+ <module name="SuppressionCommentFilter">
+ <property name="offCommentFormat" value="CHECKSTYLE:OFF IndentationCheck"/>
+ <property name="onCommentFormat" value="CHECKSTYLE:ON IndentationCheck"/>
+ <property name="checkFormat" value="IndentationCheck"/>
+ </module>
+ <module name="SuppressionCommentFilter">
+ <property name="offCommentFormat" value="CHECKSTYLE:OFF Generated code"/>
+ <property name="onCommentFormat" value="CHECKSTYLE:ON Generated code"/>
+ </module>
+ <module name="FileTabCharacter">
+ <property name="severity" value="error"/>
+ </module>
+ <module name="NewlineAtEndOfFile">
+ <property name="severity" value="error"/>
+ </module>
+ <module name="RegexpSingleline">
+ <property name="severity" value="error"/>
+ <property name="format" value="[ \t]+$"/>
+ <property name="message" value="Trailing whitespace"/>
+ </module>
+ <module name="RegexpHeader">
+ <property name="severity" value="error"/>
+ <message key="header.mismatch"
+ value="Android Copyright header seems to be incorrect. Expected ''{0}'' on this line."/>
+ <property name="header" value="^/\*\n \* Copyright \([Cc]\) [0-9]{4} The Android Open Source Project\n \*\n \* Licensed under the Apache License, Version 2\.0 \(the \"License\"\);\n \* you may not use this file except in compliance with the License.\n \* You may obtain a copy of the License at\n \*\n \* https:\/\/ptop.only.wip.la:443\/http\/www\.apache\.org\/licenses\/LICENSE-2\.0\n \*\n \* Unless required by applicable law or agreed to in writing, software\n \* distributed under the License is distributed on an "AS IS" BASIS,\n \* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.\n \* See the License for the specific language governing permissions and\n \* limitations under the License\.\n \*\/" />
+ </module>
+</module>
diff --git a/lifecycle/runtime/.gitignore b/lifecycle/runtime/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/lifecycle/runtime/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/lifecycle/runtime/build.gradle b/lifecycle/runtime/build.gradle
new file mode 100644
index 0000000..8e6883a
--- /dev/null
+++ b/lifecycle/runtime/build.gradle
@@ -0,0 +1,34 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 25
+ buildToolsVersion "25.0.0"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 25
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
+}
+
+dependencies {
+ compile project(":common")
+ // using classes like Pair from support core allows Lifecycle to be compatible w/ junit tests
+ compile "com.android.support:support-core-utils:25.0.0"
+ testCompile 'junit:junit:4.12'
+ testCompile "org.mockito:mockito-core:1.9.5"
+}
diff --git a/lifecycle/runtime/proguard-rules.pro b/lifecycle/runtime/proguard-rules.pro
new file mode 100644
index 0000000..b7210d1
--- /dev/null
+++ b/lifecycle/runtime/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/yboyar/android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/lifecycle/runtime/src/main/AndroidManifest.xml b/lifecycle/runtime/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7e2ad1f
--- /dev/null
+++ b/lifecycle/runtime/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.support.lifecycle">
+</manifest>
diff --git a/lifecycle/runtime/src/main/java/com/android/support/lifecycle/LifecycleRegistry.java b/lifecycle/runtime/src/main/java/com/android/support/lifecycle/LifecycleRegistry.java
new file mode 100644
index 0000000..164d17c
--- /dev/null
+++ b/lifecycle/runtime/src/main/java/com/android/support/lifecycle/LifecycleRegistry.java
@@ -0,0 +1,115 @@
+/*
+ * 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 com.android.support.lifecycle;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+
+/**
+ * An implementation of {@link Lifecycle} that can handle multiple observers.
+ * <p>
+ * It is used by Fragments and Support Library Activities. You can also directly use it if you have
+ * a custom LifecycleProvider.
+ */
+@SuppressWarnings("WeakerAccess")
+public class LifecycleRegistry implements Lifecycle {
+ static final String TAG = "LifecycleRegistry";
+ /**
+ * Custom list that keeps observers and can handle removals / additions during traversal.
+ */
+ private ObserverList mObserverList = new ObserverList();
+
+ /**
+ * Latest state that was provided via {@link #setCurrentState(int)}.
+ */
+ @State
+ private int mCurrentState;
+
+ /**
+ * Previously set state. This allows us to re-use the dispatcher while traversing listeners.
+ */
+ @VisibleForTesting
+ private int mPrevState;
+
+ /**
+ * The provider that owns this Lifecycle.
+ */
+ private final LifecycleProvider mLifecycleProvider;
+
+ private final ObserverList.Callback mDispatchCallback = new ObserverList.Callback() {
+ @Override
+ public void run(GenericLifecycleObserver observer) {
+ observer.onStateChanged(mLifecycleProvider, mPrevState);
+ }
+ };
+
+ /**
+ * Creates a new LifecycleRegistry for the given provider.
+ * <p>
+ * You should usually create this inside your LifecycleProvider class's constructor and hold
+ * onto the same instance.
+ *
+ * @param provider The owner LifecycleProvider
+ * @param initialState The start state.
+ */
+ public LifecycleRegistry(@NonNull LifecycleProvider provider, @State int initialState) {
+ mCurrentState = initialState;
+ mLifecycleProvider = provider;
+ }
+
+ /**
+ * Sets the current state and notifies the observers.
+ * <p>
+ * Note that if the {@code currentState} is the same state as the last call to this method,
+ * calling this method has no effect.
+ *
+ * @param currentState The updated state of the LifecycleProvider.
+ */
+ public void setCurrentState(@State int currentState) {
+ if (mCurrentState == currentState) {
+ return;
+ }
+ mPrevState = mCurrentState;
+ mCurrentState = currentState;
+ mObserverList.forEach(mDispatchCallback);
+ }
+
+ @Override
+ public void addObserver(LifecycleObserver observer) {
+ mObserverList.add(observer);
+ }
+
+ @Override
+ public void removeObserver(LifecycleObserver observer) {
+ mObserverList.remove(observer);
+ }
+
+ /**
+ * The number of observers.
+ *
+ * @return The number of observers.
+ */
+ public int size() {
+ return mObserverList.size();
+ }
+
+ @Override
+ @State
+ public int getCurrentState() {
+ return mCurrentState;
+ }
+}
diff --git a/lifecycle/runtime/src/main/java/com/android/support/lifecycle/ObserverList.java b/lifecycle/runtime/src/main/java/com/android/support/lifecycle/ObserverList.java
new file mode 100644
index 0000000..e1e8d5a
--- /dev/null
+++ b/lifecycle/runtime/src/main/java/com/android/support/lifecycle/ObserverList.java
@@ -0,0 +1,137 @@
+/*
+ * 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 com.android.support.lifecycle;
+
+import android.support.v4.util.Pair;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+class ObserverList {
+ private boolean mLocked = false;
+ private static final Object TO_BE_ADDED = new Object();
+ private static final Object TO_BE_REMOVED = new Object();
+ private List<Pair<LifecycleObserver, Object>> mPendingModifications = new ArrayList<>(0);
+ private List<Pair<LifecycleObserver, GenericLifecycleObserver>> mData = new ArrayList<>(5);
+
+ private static final String ERR_RE_ADD = "Trying to re-add an already existing observer. "
+ + "Ignoring the call for ";
+ private static final String WARN_RE_REMOVE = "Trying to remove a non-existing observer. ";
+
+ private boolean exists(LifecycleObserver observer) {
+ final int size = mData.size();
+ for (int i = 0; i < size; i++) {
+ Pair<LifecycleObserver, GenericLifecycleObserver> pair = mData.get(i);
+ if (pair.first == observer) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasPendingRemoval(LifecycleObserver observer) {
+ final int size = mPendingModifications.size();
+ for (int i = 0; i < size; i++) {
+ Pair<LifecycleObserver, Object> pair = mPendingModifications.get(i);
+ if (pair.first == observer && pair.second == TO_BE_REMOVED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void add(LifecycleObserver observer) {
+ if (!mLocked) {
+ addInternal(observer);
+ return;
+ }
+ mPendingModifications.add(new Pair<>(observer, TO_BE_ADDED));
+ }
+
+ private void addInternal(LifecycleObserver observer) {
+ if (exists(observer)) {
+ Log.e(LifecycleRegistry.TAG, ERR_RE_ADD + observer);
+ return;
+ }
+ GenericLifecycleObserver genericObserver = Lifecycling.getCallback(observer);
+ mData.add(new Pair<>(observer, genericObserver));
+ }
+
+ void remove(LifecycleObserver observer) {
+ if (!mLocked) {
+ removeInternal(observer);
+ return;
+ }
+ mPendingModifications.add(new Pair<>(observer, TO_BE_REMOVED));
+ }
+
+ private void removeInternal(LifecycleObserver observer) {
+ Iterator<Pair<LifecycleObserver, GenericLifecycleObserver>> iterator = mData.iterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().first == observer) {
+ iterator.remove();
+ return;
+ }
+ }
+ Log.w(LifecycleRegistry.TAG, WARN_RE_REMOVE + observer);
+ }
+
+ void forEach(Callback func) {
+ mLocked = true;
+ try {
+ final int size = mData.size();
+ for (int i = 0; i < size; i++) {
+ Pair<LifecycleObserver, GenericLifecycleObserver> pair = mData.get(i);
+ if (hasPendingRemoval(pair.first)) {
+ continue;
+ }
+ func.run(pair.second);
+ }
+ } finally {
+ mLocked = false;
+ syncPending();
+ }
+ }
+
+ private void syncPending() {
+ // apply everything instead of trying to get clever.
+ // it is unlikely for someone to add/rm same item and when that happens, it is important
+ // to be consistent in the behavior
+ final int size = mPendingModifications.size();
+ //noinspection StatementWithEmptyBody
+ for (int i = 0; i < size; i++) {
+ Pair<LifecycleObserver, Object> pair = mPendingModifications.get(i);
+ if (pair.second == TO_BE_REMOVED) {
+ removeInternal(pair.first);
+ } else { // TO_BE_ADDED
+ addInternal(pair.first);
+ }
+ }
+ mPendingModifications.clear();
+ }
+
+ int size() {
+ return mData.size();
+ }
+
+ interface Callback {
+ void run(GenericLifecycleObserver observer);
+ }
+
+}
diff --git a/lifecycle/runtime/src/test/java/com/android/support/lifecycle/LifecycleRegistryTest.java b/lifecycle/runtime/src/test/java/com/android/support/lifecycle/LifecycleRegistryTest.java
new file mode 100644
index 0000000..dd52771
--- /dev/null
+++ b/lifecycle/runtime/src/test/java/com/android/support/lifecycle/LifecycleRegistryTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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 com.android.support.lifecycle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class LifecycleRegistryTest {
+ private LifecycleProvider mLifecycleProvider;
+ private Lifecycle mLifecycle;
+ private LifecycleRegistry mRegistry;
+ @Before
+ public void init() {
+ mLifecycleProvider = mock(LifecycleProvider.class);
+ mLifecycle = mock(Lifecycle.class);
+ when(mLifecycleProvider.getLifecycle()).thenReturn(mLifecycle);
+ mRegistry = new LifecycleRegistry(mLifecycleProvider, Lifecycle.INITIALIZED);
+ }
+ @Test
+ public void addRemove() {
+ LifecycleObserver observer = mock(LifecycleObserver.class);
+ mRegistry.addObserver(observer);
+ assertThat(mRegistry.size(), is(1));
+ mRegistry.removeObserver(observer);
+ assertThat(mRegistry.size(), is(0));
+ }
+
+ @Test
+ public void addGenericAndObserve() {
+ GenericLifecycleObserver generic = mock(GenericLifecycleObserver.class);
+ mRegistry.addObserver(generic);
+ setState(Lifecycle.CREATED);
+ verify(generic).onStateChanged(mLifecycleProvider, Lifecycle.INITIALIZED);
+ reset(generic);
+ setState(Lifecycle.CREATED);
+ verify(generic, never()).onStateChanged(mLifecycleProvider, Lifecycle.INITIALIZED);
+ }
+
+ @Test
+ public void addRegularClass() {
+ TestObserver testObserver = mock(TestObserver.class);
+ mRegistry.addObserver(testObserver);
+ setState(Lifecycle.STARTED);
+ verify(testObserver, never()).onStopped();
+ setState(Lifecycle.STOPPED);
+ verify(testObserver).onStopped();
+ }
+
+ @Test
+ public void add2RemoveOne() {
+ TestObserver observer1 = mock(TestObserver.class);
+ TestObserver observer2 = mock(TestObserver.class);
+ TestObserver observer3 = mock(TestObserver.class);
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+ mRegistry.addObserver(observer3);
+
+ setState(Lifecycle.STOPPED);
+
+ verify(observer1).onStopped();
+ verify(observer2).onStopped();
+ verify(observer3).onStopped();
+ reset(observer1, observer2, observer3);
+
+ mRegistry.removeObserver(observer2);
+ setState(Lifecycle.PAUSED);
+
+ setState(Lifecycle.STOPPED);
+ verify(observer1).onStopped();
+ verify(observer2, never()).onStopped();
+ verify(observer3).onStopped();
+ }
+
+ @Test
+ public void removeWhileTraversing() {
+ final TestObserver observer2 = mock(TestObserver.class);
+ TestObserver observer1 = spy(new TestObserver() {
+ @Override
+ public void onStopped() {
+ mRegistry.removeObserver(observer2);
+ }
+ });
+ mRegistry.addObserver(observer1);
+ mRegistry.addObserver(observer2);
+ setState(Lifecycle.STOPPED);
+ verify(observer2, never()).onStopped();
+ verify(observer1).onStopped();
+ }
+
+ private void setState(@Lifecycle.State int state) {
+ when(mLifecycle.getCurrentState()).thenReturn(state);
+ mRegistry.setCurrentState(state);
+ }
+
+ private interface TestObserver extends LifecycleObserver {
+ @OnState(Lifecycle.STOPPED)
+ void onStopped();
+ }
+}
diff --git a/lifecycle/runtime/src/test/java/com/android/support/lifecycle/ObserverListTest.java b/lifecycle/runtime/src/test/java/com/android/support/lifecycle/ObserverListTest.java
new file mode 100644
index 0000000..1ba4902
--- /dev/null
+++ b/lifecycle/runtime/src/test/java/com/android/support/lifecycle/ObserverListTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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 com.android.support.lifecycle;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(JUnit4.class)
+public class ObserverListTest {
+ private ObserverList mObserverList;
+
+ @Before
+ public void init() {
+ mObserverList = new ObserverList();
+ }
+
+ @Test
+ public void add() {
+ GenericLifecycleObserver observer = mock(GenericLifecycleObserver.class);
+ mObserverList.add(observer);
+ assertThat(mObserverList.size(), is(1));
+ assertThat(collect().get(0), is(observer));
+ mObserverList.remove(observer);
+ assertThat(mObserverList.size(), is(0));
+ }
+
+ @Test
+ public void addTwice() {
+ GenericLifecycleObserver observer = mock(GenericLifecycleObserver.class);
+ mObserverList.add(observer);
+ assertThat(mObserverList.size(), is(1));
+ mObserverList.add(observer);
+ assertThat(mObserverList.size(), is(1));
+ }
+
+ @Test
+ public void addTwiceWhileIterating() {
+ final ObserveAll observer = spy(new ObserveAll() {
+ @Override
+ public void onAny() {
+ mObserverList.add(this);
+ mObserverList.add(this);
+ mObserverList.add(this);
+ }
+ });
+ mObserverList.add(observer);
+ mObserverList.forEach(new ObserverList.Callback() {
+ @Override
+ public void run(GenericLifecycleObserver ignored) {
+ observer.onAny();
+ }
+ });
+ verify(observer, times(1)).onAny();
+ Assert.assertThat(mObserverList.size(), is(1));
+ }
+
+ @Test
+ public void addRemoveAdd() {
+ final ObserveAll observer2 = mock(ObserveAll.class);
+ final ObserveAll observer1 = spy(new ObserveAll() {
+ @Override
+ public void onAny() {
+ mObserverList.remove(this);
+ mObserverList.add(this);
+ }
+ });
+ mObserverList.add(observer1);
+ mObserverList.add(observer2);
+ assertThat(collectObservers(),
+ equalTo(Arrays.asList((LifecycleObserver) observer1, observer2)));
+ mObserverList.forEach(new ObserverList.Callback() {
+ @Override
+ public void run(GenericLifecycleObserver glo) {
+ ((ObserveAll) glo.getReceiver()).onAny();
+ }
+ });
+ verify(observer1, times(1)).onAny();
+ verify(observer2, times(1)).onAny();
+ // because 1 has been removed and re-added, it should get the next event after o1
+ assertThat(collectObservers(),
+ equalTo(Arrays.asList((LifecycleObserver) observer2, observer1)));
+ }
+
+ @Test
+ public void remove() {
+ GenericLifecycleObserver observer = mock(GenericLifecycleObserver.class);
+ LifecycleObserver obj = mock(LifecycleObserver.class);
+ when(observer.getReceiver()).thenReturn(obj);
+ mObserverList.add(observer);
+ assertThat(mObserverList.size(), is(1));
+ assertThat(collect().get(0), is(observer));
+ mObserverList.remove(observer);
+ assertThat(mObserverList.size(), is(0));
+ }
+
+ @Test
+ public void removeWhileTraversing() {
+ GenericLifecycleObserver observer1 = mock(GenericLifecycleObserver.class);
+ final GenericLifecycleObserver observer2 = mock(GenericLifecycleObserver.class);
+ mObserverList.add(observer1);
+ mObserverList.add(observer2);
+ final AtomicBoolean first = new AtomicBoolean(true);
+ mObserverList.forEach(new ObserverList.Callback() {
+ @Override
+ public void run(GenericLifecycleObserver observer) {
+ if (first.getAndSet(false)) {
+ mObserverList.remove(observer2);
+ } else {
+ fail("should never receive this call");
+ }
+ }
+ });
+ }
+
+ @Test
+ public void removeObjectWithMultipleCallbacksWhenTraversing() {
+ // if the removed object has multiple callbacks and already received one, should receive
+ // all.
+ final AtomicBoolean first = new AtomicBoolean(true);
+ final StartedObserverWith2Methods observer = spy(new StartedObserverWith2Methods() {
+ @Override
+ public void onStarted1() {
+ handle();
+ }
+
+ @Override
+ public void onStarted2() {
+ handle();
+ }
+
+ private void handle() {
+ if (first.getAndSet(false)) {
+ mObserverList.remove(this);
+ }
+ }
+ });
+ mObserverList.add(observer);
+
+ final LifecycleProvider lifecycleProvider = mock(LifecycleProvider.class);
+ Lifecycle lifecycle = mock(Lifecycle.class);
+ when(lifecycleProvider.getLifecycle()).thenReturn(lifecycle);
+ when(lifecycle.getCurrentState()).thenReturn(Lifecycle.STARTED);
+
+ mObserverList.forEach(new ObserverList.Callback() {
+ @Override
+ public void run(GenericLifecycleObserver observer) {
+ observer.onStateChanged(lifecycleProvider, Lifecycle.CREATED);
+ }
+ });
+
+ verify(observer).onStarted1();
+ verify(observer).onStarted2();
+ }
+
+ private List<GenericLifecycleObserver> collect() {
+ final ArrayList<GenericLifecycleObserver> items = new ArrayList<>();
+ mObserverList.forEach(new ObserverList.Callback() {
+ @Override
+ public void run(GenericLifecycleObserver observer) {
+ items.add(observer);
+ }
+ });
+ return items;
+ }
+
+ private List<LifecycleObserver> collectObservers() {
+ final ArrayList<LifecycleObserver> items = new ArrayList<>();
+ mObserverList.forEach(new ObserverList.Callback() {
+ @Override
+ public void run(GenericLifecycleObserver observer) {
+ items.add((LifecycleObserver) observer.getReceiver());
+ }
+ });
+ return items;
+ }
+
+ @SuppressWarnings("unused")
+ private interface StartedObserverWith2Methods extends LifecycleObserver {
+ @OnState(Lifecycle.STARTED)
+ void onStarted1();
+
+ @OnState(Lifecycle.STARTED)
+ void onStarted2();
+ }
+
+ private interface ObserveAll extends LifecycleObserver {
+ @OnState(Lifecycle.ANY)
+ void onAny();
+ }
+}
diff --git a/lifecycle/settings.gradle b/lifecycle/settings.gradle
index 498bd72..2643340 100644
--- a/lifecycle/settings.gradle
+++ b/lifecycle/settings.gradle
@@ -1,2 +1,4 @@
+include ':extensions'
+include ':runtime'
include 'common'
include 'compiler'
\ No newline at end of file